@harborppm/cli 0.82.0 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/app.mjs +227 -98
  2. package/package.json +1 -1
package/dist/app.mjs CHANGED
@@ -3085,7 +3085,36 @@ var init_barnacles = __esmMin((() => {
3085
3085
  plural$7 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
3086
3086
  }));
3087
3087
 
3088
+ // src/lib/local-day.ts
3089
+ init_indexes();
3090
+ init_premortem();
3091
+ init_barnacles();
3092
+ init_format();
3093
+ /**
3094
+ * The day a saved moment fell on where this person is: "2026-11-05T01:30:00Z"
3095
+ * is still the 4th in New York. A plain day ("2026-11-05") is itself.
3096
+ */
3097
+ function localDay(stamp) {
3098
+ if (/^\d{4}-\d{2}-\d{2}$/.test(stamp)) return stamp;
3099
+ const d = new Date(stamp);
3100
+ return Number.isNaN(d.getTime()) ? stamp.slice(0, 10) : todayISO(d);
3101
+ }
3102
+
3088
3103
  // src/lib/task-keys.ts
3104
+ /**
3105
+ * Short keys for tasks: a project's key and a task's number within it,
3106
+ * "RR-42". They're what someone types in a branch name, a pull request or a
3107
+ * commit, so once given they don't change, except that a task moved to
3108
+ * another project takes that project's next number.
3109
+ *
3110
+ * The server settles keys on every write (a save, an agent's change, an
3111
+ * intake form, the nightly sweep) and on first load: projects without a key
3112
+ * get one, tasks without a number get the next ones in the order they were
3113
+ * made, and when two tabs gave out the same number at once the task made
3114
+ * first keeps it. In the browser, `write()` numbers only the tasks it adds or
3115
+ * moves (`numberNewTasks`), so a tab never invents numbers for old tasks that
3116
+ * the server would number differently.
3117
+ */
3089
3118
  /** "Ridge Runner" → "RR", "Website" → "WEB", "Q4 launch plan" → "QLP". */
3090
3119
  function baseKey(name) {
3091
3120
  const words = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^A-Za-z0-9 ]+/g, " ").split(/\s+/).filter((w) => /[A-Za-z]/.test(w));
@@ -3248,7 +3277,6 @@ function findByKey(data, text) {
3248
3277
  }
3249
3278
  var MAX_KEY, KEY, made, kept, KEY_TEXT;
3250
3279
  var init_task_keys = __esmMin((() => {
3251
- init_indexes();
3252
3280
  MAX_KEY = 5;
3253
3281
  KEY = /^[A-Z][A-Z0-9]{0,9}$/;
3254
3282
  made = (a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
@@ -3365,11 +3393,9 @@ var init_charter_lines = __esmMin((() => {
3365
3393
  }));
3366
3394
 
3367
3395
  // src/lib/charter.ts
3368
- init_barnacles();
3369
3396
  init_thanks();
3370
3397
  init_date_fns();
3371
3398
  init_date_text();
3372
- init_premortem();
3373
3399
  init_charter_lines();
3374
3400
  const CHARTER_LABEL = {
3375
3401
  why: "Why",
@@ -3487,7 +3513,7 @@ init_date_fns();
3487
3513
  init_blockers();
3488
3514
  init_date_text();
3489
3515
  init_premortem();
3490
- const day$8 = (iso) => parseIso(iso.slice(0, 10));
3516
+ const day$8 = (iso) => parseIso(localDay(iso));
3491
3517
  const between = (a, b) => differenceInCalendarDays(day$8(b), day$8(a));
3492
3518
  /** When the project was marked Done: its last move to Done in its health history. */
3493
3519
  function finishedOn(project, tasks) {
@@ -3509,7 +3535,7 @@ function voyageLog(data, projectId) {
3509
3535
  return {
3510
3536
  task: t,
3511
3537
  daysLate: between(t.dueDate, t.completedAt),
3512
- waitedOn: blockerIds(t).map((id) => byId.get(id)).filter((b) => Boolean(b?.completedAt && b.completedAt.slice(0, 10) > t.dueDate.slice(0, 10))).sort((a, b) => b.completedAt.localeCompare(a.completedAt))[0] ?? null
3538
+ waitedOn: blockerIds(t).map((id) => byId.get(id)).filter((b) => Boolean(b?.completedAt && localDay(b.completedAt) > t.dueDate.slice(0, 10))).sort((a, b) => b.completedAt.localeCompare(a.completedAt))[0] ?? null
3513
3539
  };
3514
3540
  }).filter((s) => s.daysLate > 0).sort((a, b) => b.daysLate - a.daysLate).slice(0, 3);
3515
3541
  const raid = data.raidItems.filter((r) => r.projectId === projectId);
@@ -5626,6 +5652,7 @@ const ATTENTION = ["at-risk", "off-track"];
5626
5652
  function matches$1(row, f) {
5627
5653
  if (f.health === "attention" && !ATTENTION.includes(row.health)) return false;
5628
5654
  if (f.ownerId && row.ownerId !== f.ownerId) return false;
5655
+ if (f.hideDone && row.health === "done") return false;
5629
5656
  return true;
5630
5657
  }
5631
5658
  function milestoneOf(m, today) {
@@ -5642,7 +5669,8 @@ function milestoneOf(m, today) {
5642
5669
  /**
5643
5670
  * Programs by start date, each followed by its projects by start date unless
5644
5671
  * collapsed. A filter keeps a program when it or any of its projects match,
5645
- * and keeps the projects that match.
5672
+ * and keeps the projects that match. So with done work hidden, a Done
5673
+ * program still heads any of its projects that aren't done.
5646
5674
  */
5647
5675
  function roadmapRows(data, portfolioId, filter, collapsed, today) {
5648
5676
  const programs = data.programs.filter((p) => p.portfolioId === portfolioId).sort((a, b) => a.startDate.localeCompare(b.startDate) || byName(a.name, b.name));
@@ -5666,7 +5694,7 @@ function roadmapRows(data, portfolioId, filter, collapsed, today) {
5666
5694
  baselineEnd: null,
5667
5695
  slip: null,
5668
5696
  waitsOn: blockerIds(program).filter((id) => programIds.has(id)),
5669
- milestones: data.milestones.filter((m) => m.programId === program.id).sort((a, b) => a.dueDate.localeCompare(b.dueDate)).map((m) => milestoneOf(m, today)),
5697
+ milestones: data.milestones.filter((m) => m.programId === program.id && !(filter.hideDone && m.done)).sort((a, b) => a.dueDate.localeCompare(b.dueDate)).map((m) => milestoneOf(m, today)),
5670
5698
  projectCount: projects.length
5671
5699
  });
5672
5700
  if (collapsed.has(program.id)) continue;
@@ -5689,6 +5717,18 @@ function roadmapRows(data, portfolioId, filter, collapsed, today) {
5689
5717
  }
5690
5718
  return out;
5691
5719
  }
5720
+ /** How many programs and projects on this roadmap are Done: what hiding done work tucks away. */
5721
+ function roadmapDoneCount(data, portfolioId) {
5722
+ const programIds = /* @__PURE__ */ new Set();
5723
+ let count = 0;
5724
+ for (const p of data.programs) {
5725
+ if (p.portfolioId !== portfolioId) continue;
5726
+ programIds.add(p.id);
5727
+ if (p.health === "done") count++;
5728
+ }
5729
+ for (const p of data.projects) if (programIds.has(p.programId) && p.health === "done") count++;
5730
+ return count;
5731
+ }
5692
5732
 
5693
5733
  // src/lib/search.ts
5694
5734
  function tokensOf(query) {
@@ -8952,7 +8992,7 @@ function TaskLine({ task, projects, people, today, selected, picked = false, wid
8952
8992
  });
8953
8993
  }
8954
8994
  /** Up/down within a list of `length`, wrapping neither way. */
8955
- function step(at, delta, length) {
8995
+ function step$1(at, delta, length) {
8956
8996
  if (length === 0) return 0;
8957
8997
  return Math.max(0, Math.min(length - 1, at + delta));
8958
8998
  }
@@ -9009,8 +9049,8 @@ function CalendarView({ active, width, height, go }) {
9009
9049
  useInput((input, key) => {
9010
9050
  if (inList) {
9011
9051
  if (key.escape) setInList(false);
9012
- else if (key.upArrow || input === "k") setAt((i) => step(i, -1, agenda.length));
9013
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, agenda.length));
9052
+ else if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, agenda.length));
9053
+ else if (key.downArrow || input === "j") setAt((i) => step$1(i, 1, agenda.length));
9014
9054
  else if (key.return && selected) go.openTask(selected.id);
9015
9055
  else if (input === "x" && selected) moveTask(store, go, selected, "done");
9016
9056
  return;
@@ -9699,12 +9739,12 @@ function HistoryView({ active, width, height, go }) {
9699
9739
  });
9700
9740
  };
9701
9741
  useInput((input, key) => {
9702
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, rows.length));
9742
+ if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, rows.length));
9703
9743
  else if (key.downArrow || input === "j") {
9704
9744
  if (at >= rows.length - 2) loadMore();
9705
- setAt((i) => step(i, 1, rows.length));
9706
- } else if (key.pageDown) setAt((i) => step(i, 10, rows.length));
9707
- else if (key.pageUp) setAt((i) => step(i, -10, rows.length));
9745
+ setAt((i) => step$1(i, 1, rows.length));
9746
+ } else if (key.pageDown) setAt((i) => step$1(i, 10, rows.length));
9747
+ else if (key.pageUp) setAt((i) => step$1(i, -10, rows.length));
9708
9748
  else if (input === "s") go.pick("History of", [
9709
9749
  ...portfolios.map((p) => ({
9710
9750
  label: p.name,
@@ -9824,6 +9864,36 @@ function HistoryView({ active, width, height, go }) {
9824
9864
  });
9825
9865
  }
9826
9866
 
9867
+ // src/lib/read-marks.ts
9868
+ /**
9869
+ * Inbox read marks. A harbor keeps one list of them (`readPingIds`), shared
9870
+ * by everyone in it, so each mark names whose it is: reading a mention that
9871
+ * names Sam and Aya clears it from Sam's Inbox only. Marks written before
9872
+ * they were personal (the bare line id) still count as read. Pure; the Inbox
9873
+ * makes the marks it hands the store, so none of this is in the first load.
9874
+ */
9875
+ /** FNV-1a over the text, as eight hex digits. */
9876
+ function hash8(text) {
9877
+ let h = 2166136261;
9878
+ for (let i = 0; i < text.length; i++) {
9879
+ h ^= text.charCodeAt(i);
9880
+ h = Math.imul(h, 16777619);
9881
+ }
9882
+ return (h >>> 0).toString(16).padStart(8, "0");
9883
+ }
9884
+ /** A saved row id holds 64 characters. */
9885
+ const MAX_MARK = 64;
9886
+ /** The mark that says this person has read this Inbox line. */
9887
+ function readMark(personId, pingId) {
9888
+ const mark = `r${hash8(personId)}:${pingId}`;
9889
+ return mark.length <= MAX_MARK ? mark : `r${hash8(personId)}:#${hash8(pingId)}`;
9890
+ }
9891
+ /** Whether a person has read a line, given the harbor's marks. */
9892
+ function readBy(marks, personId) {
9893
+ const set = marks instanceof Set ? marks : new Set(marks ?? []);
9894
+ return (pingId) => set.has(pingId) || set.has(readMark(personId, pingId));
9895
+ }
9896
+
9827
9897
  // src/lib/inbox.ts
9828
9898
  init_date_fns();
9829
9899
  init_indexes();
@@ -10091,22 +10161,22 @@ function InboxView({ active, width, height, go }) {
10091
10161
  me,
10092
10162
  store
10093
10163
  ]);
10094
- const read = new Set(readIds ?? []);
10095
- const unread = pings.filter((p) => !read.has(p.id)).length;
10164
+ const read = readBy(readIds, me);
10165
+ const unread = pings.filter((p) => !read(p.id)).length;
10096
10166
  const [at, setAt] = useState(0);
10097
10167
  const ping = pings[Math.min(at, pings.length - 1)];
10098
10168
  const open = (p) => {
10099
- store.getState().readPing(p.id);
10169
+ store.getState().readPing(readMark(me, p.id));
10100
10170
  if (p.taskId) go.openTask(p.taskId);
10101
10171
  else if (p.raidId) go.openPanel("raid", p.raidId);
10102
10172
  else if (p.programId) go.openProgram(p.programId);
10103
10173
  };
10104
10174
  useInput((input, key) => {
10105
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, pings.length));
10106
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, pings.length));
10175
+ if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, pings.length));
10176
+ else if (key.downArrow || input === "j") setAt((i) => step$1(i, 1, pings.length));
10107
10177
  else if (key.return && ping) open(ping);
10108
- else if (input === " " && ping) store.getState().readPing(ping.id);
10109
- else if (input === "a") for (const p of pings) store.getState().readPing(p.id);
10178
+ else if (input === " " && ping) store.getState().readPing(readMark(me, ping.id));
10179
+ else if (input === "a") store.getState().catchUp(pings.map((p) => readMark(me, p.id)));
10110
10180
  }, { isActive: active });
10111
10181
  const view = windowOf(pings, at, Math.max(1, height - 2));
10112
10182
  return /* @__PURE__ */ jsxs(Box, {
@@ -10125,7 +10195,7 @@ function InboxView({ active, width, height, go }) {
10125
10195
  }) : null,
10126
10196
  view.items.map((p, i) => {
10127
10197
  const on = active && view.start + i === at;
10128
- const fresh = !read.has(p.id);
10198
+ const fresh = !read(p.id);
10129
10199
  return /* @__PURE__ */ jsxs(Text, {
10130
10200
  color: on ? theme.selected : void 0,
10131
10201
  bold: on || fresh,
@@ -12108,10 +12178,10 @@ function RowList({ title, right, rows, empty, active, width, height, onOpen, key
12108
12178
  const [at, setAt] = useState(0);
12109
12179
  const row = rows[Math.min(at, rows.length - 1)];
12110
12180
  useInput((input, key) => {
12111
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, rows.length));
12112
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, rows.length));
12113
- else if (key.pageDown) setAt((i) => step(i, 10, rows.length));
12114
- else if (key.pageUp) setAt((i) => step(i, -10, rows.length));
12181
+ if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, rows.length));
12182
+ else if (key.downArrow || input === "j") setAt((i) => step$1(i, 1, rows.length));
12183
+ else if (key.pageDown) setAt((i) => step$1(i, 10, rows.length));
12184
+ else if (key.pageUp) setAt((i) => step$1(i, -10, rows.length));
12115
12185
  else if (key.return && row) onOpen(row);
12116
12186
  else keys[input]?.(row);
12117
12187
  }, { isActive: active });
@@ -12741,8 +12811,8 @@ function SearchBox({ width, onOpen, onClose }) {
12741
12811
  ]);
12742
12812
  useInput((input, key) => {
12743
12813
  if (key.escape) return onClose();
12744
- if (key.upArrow) return setAt((i) => step(i, -1, hits.length));
12745
- if (key.downArrow) return setAt((i) => step(i, 1, hits.length));
12814
+ if (key.upArrow) return setAt((i) => step$1(i, -1, hits.length));
12815
+ if (key.downArrow) return setAt((i) => step$1(i, 1, hits.length));
12746
12816
  if (key.return) {
12747
12817
  const hit = hits[Math.min(at, hits.length - 1)];
12748
12818
  if (hit) onOpen(hit.item.id);
@@ -13000,8 +13070,8 @@ function Palette({ width, base, open, onClose }) {
13000
13070
  const hits = useMemo(() => matchCommands(all, query, 12), [all, query]);
13001
13071
  useInput((input, key) => {
13002
13072
  if (key.escape) return onClose();
13003
- if (key.upArrow) return setAt((i) => step(i, -1, hits.length));
13004
- if (key.downArrow) return setAt((i) => step(i, 1, hits.length));
13073
+ if (key.upArrow) return setAt((i) => step$1(i, -1, hits.length));
13074
+ if (key.downArrow) return setAt((i) => step$1(i, 1, hits.length));
13005
13075
  if (key.return) {
13006
13076
  const hit = hits[Math.min(at, hits.length - 1)];
13007
13077
  onClose();
@@ -13108,8 +13178,8 @@ function PremortemPanel({ id, width, height, go, active, onClose }) {
13108
13178
  useInput((input, key) => {
13109
13179
  if (key.escape) return onClose();
13110
13180
  if (!project || !pm) return;
13111
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, rows.length));
13112
- if (key.downArrow || input === "j") return setAt((i) => step(i, 1, rows.length));
13181
+ if (key.upArrow || input === "k") return setAt((i) => step$1(i, -1, rows.length));
13182
+ if (key.downArrow || input === "j") return setAt((i) => step$1(i, 1, rows.length));
13113
13183
  if (key.return && row?.risk) return go.openPanel("raid", row.risk.id);
13114
13184
  if (viewOnly) return;
13115
13185
  if (pm.phase === "none") {
@@ -23485,25 +23555,39 @@ var init_focus = __esmMin((() => {
23485
23555
  // src/lib/recurrence.ts
23486
23556
  /** The due date one interval after `from`. */
23487
23557
  function nextDueDate(from, repeat) {
23488
- const d = /* @__PURE__ */ new Date(`${from}T12:00:00`);
23489
- if (repeat === "weekly") return iso(addDays(d, 7));
23490
- if (repeat === "biweekly") return iso(addDays(d, 14));
23491
- return iso(addMonths$1(d, 1));
23558
+ return iso(step(/* @__PURE__ */ new Date(`${from}T12:00:00`), repeat, 1));
23559
+ }
23560
+ /**
23561
+ * The next date in a series that started on `due`, at least one interval
23562
+ * on and no earlier than `today`. A weekly Monday meeting ticked off late on
23563
+ * Wednesday comes back the next Monday, so the series keeps its day; a
23564
+ * monthly one on the 31st keeps the 31st where the month has one.
23565
+ */
23566
+ function nextSeriesDate(due, repeat, today) {
23567
+ const start = /* @__PURE__ */ new Date(`${due}T12:00:00`);
23568
+ let n = 1;
23569
+ let next = iso(step(start, repeat, n));
23570
+ while (next < today && n < 2e3) next = iso(step(start, repeat, ++n));
23571
+ return next;
23492
23572
  }
23493
23573
  /**
23494
23574
  * The next occurrence of a repeating task once this one is done: same
23495
- * title, owner, priority and checklist (unticked), due one interval after
23496
- * the old due date (or after today when it had none), back in To do.
23497
- * Null when the task does not repeat.
23575
+ * title, owner, priority and checklist (unticked), back in To do. It is due
23576
+ * on the series' next date that isn't already past (see `nextSeriesDate`),
23577
+ * or one interval after today when the task had no due date. Null when the
23578
+ * task does not repeat, or when `siblings` (the project's tasks) already
23579
+ * hold an open copy of the series due on or after that date, so finishing,
23580
+ * reopening and finishing again doesn't queue a second one.
23498
23581
  */
23499
- function nextOccurrence(task, id, today, createdAt) {
23582
+ function nextOccurrence(task, id, today, createdAt, siblings = []) {
23500
23583
  if (!task.repeat) return null;
23501
- const anchor = task.dueDate && task.dueDate >= today ? task.dueDate : today;
23584
+ const dueDate = task.dueDate ? nextSeriesDate(task.dueDate, task.repeat, today) : nextDueDate(today, task.repeat);
23585
+ if (siblings.some((t) => t.status !== "done" && t.repeat === task.repeat && t.title === task.title && (t.dueDate ?? "") >= dueDate)) return null;
23502
23586
  return {
23503
23587
  ...task,
23504
23588
  id,
23505
23589
  status: "todo",
23506
- dueDate: nextDueDate(anchor, task.repeat),
23590
+ dueDate,
23507
23591
  createdAt,
23508
23592
  completedAt: null,
23509
23593
  blockedById: null,
@@ -23514,7 +23598,7 @@ function nextOccurrence(task, id, today, createdAt) {
23514
23598
  }))
23515
23599
  };
23516
23600
  }
23517
- var REPEAT_LABEL, iso;
23601
+ var REPEAT_LABEL, iso, step;
23518
23602
  var init_recurrence = __esmMin((() => {
23519
23603
  init_date_fns();
23520
23604
  REPEAT_LABEL = {
@@ -23525,6 +23609,7 @@ var init_recurrence = __esmMin((() => {
23525
23609
  iso = (d) => {
23526
23610
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
23527
23611
  };
23612
+ step = (d, repeat, n) => repeat === "weekly" ? addDays(d, 7 * n) : repeat === "biweekly" ? addDays(d, 14 * n) : addMonths$1(d, n);
23528
23613
  }));
23529
23614
 
23530
23615
  // src/lib/assignment.ts
@@ -23700,7 +23785,7 @@ function tasksSlice({ set, get, write }) {
23700
23785
  return stampAssignment(row, finalTask, get().currentUserId, stamp());
23701
23786
  });
23702
23787
  const finished = task && patch.status === "done" && task.status !== "done" ? tasks.find((t) => t.id === id) : void 0;
23703
- const follow = finished ? nextOccurrence(finished, uid("t"), todayISO(), stamp()) : null;
23788
+ const follow = finished ? nextOccurrence(finished, uid("t"), todayISO(), stamp(), tasksOf(tasks, finished.projectId)) : null;
23704
23789
  write({
23705
23790
  tasks: follow ? [...tasks, follow] : tasks,
23706
23791
  comments: newComments.length ? [...get().comments, ...newComments] : get().comments,
@@ -23732,7 +23817,7 @@ function tasksSlice({ set, get, write }) {
23732
23817
  created: false,
23733
23818
  currentUserId: get().currentUserId
23734
23819
  });
23735
- const follow = status === "done" ? nextOccurrence(finalRow, uid("t"), todayISO(), stamp()) : null;
23820
+ const follow = status === "done" ? nextOccurrence(finalRow, uid("t"), todayISO(), stamp(), tasksOf(get().tasks, task.projectId)) : null;
23736
23821
  const moved = get().tasks.map((row) => row.id === id ? finalRow : row);
23737
23822
  write({
23738
23823
  tasks: follow ? [...moved, follow] : moved,
@@ -23851,6 +23936,19 @@ function tasksSlice({ set, get, write }) {
23851
23936
  } : i)
23852
23937
  } : t) });
23853
23938
  },
23939
+ renameChecklistItem: (taskId, itemId, title) => {
23940
+ const name = title.trim();
23941
+ const item = get().tasks.find((t) => t.id === taskId)?.checklist?.find((i) => i.id === itemId);
23942
+ if (!name || !item || item.title === name) return;
23943
+ set({ tasks: get().tasks.map((t) => t.id === taskId ? {
23944
+ ...t,
23945
+ touchedAt: stamp(),
23946
+ checklist: (t.checklist ?? []).map((i) => i.id === itemId ? {
23947
+ ...i,
23948
+ title: name
23949
+ } : i)
23950
+ } : t) });
23951
+ },
23854
23952
  removeChecklistItem: (taskId, itemId) => {
23855
23953
  set({ tasks: get().tasks.map((t) => t.id === taskId ? {
23856
23954
  ...t,
@@ -23924,15 +24022,15 @@ function tasksSlice({ set, get, write }) {
23924
24022
  github: withRef(t.github, ref)
23925
24023
  } : t) });
23926
24024
  },
23927
- readPing: (id) => {
24025
+ readPing: (mark) => {
23928
24026
  const ids = get().readPingIds ?? [];
23929
- if (ids.includes(id)) return;
23930
- set({ readPingIds: [...ids, id] });
24027
+ if (ids.includes(mark)) return;
24028
+ set({ readPingIds: [...ids, mark] });
23931
24029
  },
23932
- catchUp: (ids) => {
23933
- if (ids.length === 0) return;
24030
+ catchUp: (marks) => {
24031
+ if (marks.length === 0) return;
23934
24032
  const have = new Set(get().readPingIds ?? []);
23935
- for (const id of ids) have.add(id);
24033
+ for (const mark of marks) have.add(mark);
23936
24034
  set({ readPingIds: [...have] });
23937
24035
  },
23938
24036
  tickVoyage: (ids) => {
@@ -24001,7 +24099,7 @@ function tasksSlice({ set, get, write }) {
24001
24099
  })));
24002
24100
  const final = stampAssignment(row, automated, me, at);
24003
24101
  if (final.status === "done" && row.status !== "done") {
24004
- const follow = nextOccurrence(final, uid("t"), todayISO(), at);
24102
+ const follow = nextOccurrence(final, uid("t"), todayISO(), at, [...tasksOf(s.tasks, row.projectId), ...follows]);
24005
24103
  if (follow) follows.push(follow);
24006
24104
  }
24007
24105
  return final;
@@ -24020,7 +24118,8 @@ function tasksSlice({ set, get, write }) {
24020
24118
  toggleFocus: (taskId) => {
24021
24119
  const personId = get().currentUserId;
24022
24120
  const date = todayISO();
24023
- const ids = (get().ropes ?? []).find((r) => r.personId === personId && r.date === date)?.taskIds ?? [];
24121
+ const live = byId(get().tasks);
24122
+ const ids = ((get().ropes ?? []).find((r) => r.personId === personId && r.date === date)?.taskIds ?? []).filter((id) => live.has(id));
24024
24123
  if (ids.includes(taskId)) {
24025
24124
  set({ ropes: upsertFocus(get().ropes, personId, date, ids.filter((id) => id !== taskId)) });
24026
24125
  return "removed";
@@ -24120,6 +24219,7 @@ var init_tasks = __esmMin((() => {
24120
24219
  init_work();
24121
24220
  init_shared();
24122
24221
  init_recurrence();
24222
+ init_indexes();
24123
24223
  init_assignment();
24124
24224
  init_bulk();
24125
24225
  init_task_people();
@@ -27317,8 +27417,8 @@ function DockPanel({ id, width, height, go, active, onClose }) {
27317
27417
  useInput((input, key) => {
27318
27418
  if (key.escape) return onClose();
27319
27419
  if (!project) return;
27320
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, changes.length));
27321
- if (key.downArrow || input === "j") return setAt((i) => step(i, 1, changes.length));
27420
+ if (key.upArrow || input === "k") return setAt((i) => step$1(i, -1, changes.length));
27421
+ if (key.downArrow || input === "j") return setAt((i) => step$1(i, 1, changes.length));
27322
27422
  if (input === "a" || input === "n") return newChange();
27323
27423
  if (input === "d" && changes.length) {
27324
27424
  const i = Math.min(at, changes.length - 1);
@@ -27496,8 +27596,8 @@ function LifeboatsPanel({ id, width, height, go, active, onClose }) {
27496
27596
  };
27497
27597
  useInput((input, key) => {
27498
27598
  if (key.escape) return onClose();
27499
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, lines.length));
27500
- if (key.downArrow || input === "j") return setAt((i) => step(i, 1, lines.length));
27599
+ if (key.upArrow || input === "k") return setAt((i) => step$1(i, -1, lines.length));
27600
+ if (key.downArrow || input === "j") return setAt((i) => step$1(i, 1, lines.length));
27501
27601
  if (key.return && line) return choose(line);
27502
27602
  }, { isActive: active !== false });
27503
27603
  const rows = Math.max(1, Math.floor((height - 6 - notes.length) / 3));
@@ -27629,8 +27729,8 @@ function ShorePanel({ id, width, height, go, active, onClose }) {
27629
27729
  useInput((input, key) => {
27630
27730
  if (key.escape) return onClose();
27631
27731
  if (!answer) return;
27632
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, SHORE_KINDS.length));
27633
- if (key.downArrow || input === "j") return setAt((i) => step(i, 1, SHORE_KINDS.length));
27732
+ if (key.upArrow || input === "k") return setAt((i) => step$1(i, -1, SHORE_KINDS.length));
27733
+ if (key.downArrow || input === "j") return setAt((i) => step$1(i, 1, SHORE_KINDS.length));
27634
27734
  if (viewOnly) return;
27635
27735
  if (input === "p") return go.ask("The channel's incoming-webhook address", (text) => {
27636
27736
  const url = text.trim();
@@ -27789,8 +27889,8 @@ function WelcomeView({ when, active, width, height, go, onDone }) {
27789
27889
  const look = w.lookFirst;
27790
27890
  useInput((input, key) => {
27791
27891
  if (key.escape || input === "g") return onDone();
27792
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, look.length));
27793
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, look.length));
27892
+ if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, look.length));
27893
+ else if (key.downArrow || input === "j") setAt((i) => step$1(i, 1, look.length));
27794
27894
  else if (key.return) {
27795
27895
  const l = look[at];
27796
27896
  if (!l) return onDone();
@@ -28199,11 +28299,11 @@ function ProgramView({ programId, active, width, height, go, onBack }) {
28199
28299
  return setShowDone((v) => !v);
28200
28300
  }
28201
28301
  if (side === "projects") {
28202
- if (key.upArrow || input === "k") setPAt((i) => step(i, -1, mine.length));
28203
- else if (key.downArrow || input === "j") setPAt((i) => step(i, 1, mine.length));
28302
+ if (key.upArrow || input === "k") setPAt((i) => step$1(i, -1, mine.length));
28303
+ else if (key.downArrow || input === "j") setPAt((i) => step$1(i, 1, mine.length));
28204
28304
  else if (key.return && project) go.openProject(project.id);
28205
- } else if (key.upArrow || input === "k") setMAt((i) => step(i, -1, marks.length));
28206
- else if (key.downArrow || input === "j") setMAt((i) => step(i, 1, marks.length));
28305
+ } else if (key.upArrow || input === "k") setMAt((i) => step$1(i, -1, marks.length));
28306
+ else if (key.downArrow || input === "j") setMAt((i) => step$1(i, 1, marks.length));
28207
28307
  else if (key.return && mark) go.openPanel("milestone", mark.id);
28208
28308
  else if (input === " " && mark) store.getState().toggleMilestone(mark.id);
28209
28309
  else if (input === "d" && mark) go.confirm(`Delete the milestone “${mark.name}”? u undoes it.`, () => store.getState().deleteMilestone(mark.id), "Delete");
@@ -28427,8 +28527,8 @@ function PortfolioView({ portfolioId, active, width, height, go, onBack }) {
28427
28527
  if (input === "n") return go.create("program", { portfolioId });
28428
28528
  if (input === "v") return setLayout((l) => l === "board" ? "bands" : "board");
28429
28529
  if (input === "D" && split.done.length > 0) return setShowDone((v) => !v);
28430
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, list.length));
28431
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, list.length));
28530
+ if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, list.length));
28531
+ else if (key.downArrow || input === "j") setAt((i) => step$1(i, 1, list.length));
28432
28532
  else if (key.return && program) go.openProgram(program.id);
28433
28533
  }, { isActive: active });
28434
28534
  if (!portfolio) return /* @__PURE__ */ jsx(Text, {
@@ -28817,8 +28917,8 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
28817
28917
  if (input === "v") return setMode((m) => m === "board" ? "list" : m === "list" ? "budget" : m === "budget" ? "files" : "board");
28818
28918
  if (mode === "files") {
28819
28919
  const f = files[Math.min(fileAt, files.length - 1)];
28820
- if (key.upArrow || input === "k") setFileAt((i) => step(i, -1, files.length));
28821
- else if (key.downArrow || input === "j") setFileAt((i) => step(i, 1, files.length));
28920
+ if (key.upArrow || input === "k") setFileAt((i) => step$1(i, -1, files.length));
28921
+ else if (key.downArrow || input === "j") setFileAt((i) => step$1(i, 1, files.length));
28822
28922
  else if (key.return && f) go.openTask(f.task.id);
28823
28923
  return;
28824
28924
  }
@@ -28862,8 +28962,8 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
28862
28962
  if (mode === "budget") {
28863
28963
  const entries = project ? budgetOf({ spendEntries }, project, /* @__PURE__ */ new Date()).entries : [];
28864
28964
  const entry = entries[Math.min(spendAt, entries.length - 1)];
28865
- if (key.upArrow || input === "k") setSpendAt((i) => step(i, -1, entries.length));
28866
- else if (key.downArrow || input === "j") setSpendAt((i) => step(i, 1, entries.length));
28965
+ if (key.upArrow || input === "k") setSpendAt((i) => step$1(i, -1, entries.length));
28966
+ else if (key.downArrow || input === "j") setSpendAt((i) => step$1(i, 1, entries.length));
28867
28967
  else if (input === "a" || input === "n") go.create("spend", { projectId });
28868
28968
  else if (input === "d" && entry) go.confirm(`Take out ${money(entry.amount)}${entry.note ? ` for “${entry.note}”` : ""}? u undoes it.`, () => store.getState().removeSpend(entry.id), "Take it out");
28869
28969
  return;
@@ -28879,20 +28979,20 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
28879
28979
  if (input === "x" && selected) return moveTask(store, go, selected, "done");
28880
28980
  if (input === "d" && selected) return go.confirm(`Delete ${keyOf$1({ projects }, selected)} “${selected.title}”? u undoes it.`, () => store.getState().deleteTask(selected.id), "Delete");
28881
28981
  if (mode === "board") {
28882
- if (key.leftArrow || input === "h") setCol((c) => step(c, -1, columns.length));
28883
- else if (key.rightArrow || input === "l") setCol((c) => step(c, 1, columns.length));
28982
+ if (key.leftArrow || input === "h") setCol((c) => step$1(c, -1, columns.length));
28983
+ else if (key.rightArrow || input === "l") setCol((c) => step$1(c, 1, columns.length));
28884
28984
  else if (key.upArrow || input === "k") setRowByCol((r) => ({
28885
28985
  ...r,
28886
- [col]: step(row, -1, column?.tasks.length ?? 0)
28986
+ [col]: step$1(row, -1, column?.tasks.length ?? 0)
28887
28987
  }));
28888
28988
  else if (key.downArrow || input === "j") setRowByCol((r) => ({
28889
28989
  ...r,
28890
- [col]: step(row, 1, column?.tasks.length ?? 0)
28990
+ [col]: step$1(row, 1, column?.tasks.length ?? 0)
28891
28991
  }));
28892
- } else if (key.upArrow || input === "k") setListAt((i) => step(i, -1, list.length));
28893
- else if (key.downArrow || input === "j") setListAt((i) => step(i, 1, list.length));
28894
- else if (key.pageDown) setListAt((i) => step(i, 10, list.length));
28895
- else if (key.pageUp) setListAt((i) => step(i, -10, list.length));
28992
+ } else if (key.upArrow || input === "k") setListAt((i) => step$1(i, -1, list.length));
28993
+ else if (key.downArrow || input === "j") setListAt((i) => step$1(i, 1, list.length));
28994
+ else if (key.pageDown) setListAt((i) => step$1(i, 10, list.length));
28995
+ else if (key.pageUp) setListAt((i) => step$1(i, -10, list.length));
28896
28996
  }, { isActive: active });
28897
28997
  if (!project) return /* @__PURE__ */ jsx(Box, {
28898
28998
  paddingX: 1,
@@ -29363,7 +29463,8 @@ const NAME = 26;
29363
29463
  /**
29364
29464
  * A portfolio's programs and projects across the months, as on the
29365
29465
  * website's roadmap: bars in each one's health, ◆ for milestones. [ ] for
29366
- * another portfolio, z to zoom, ←→ to page through time.
29466
+ * another portfolio, z to zoom, ←→ to page through time, D to hide or show
29467
+ * Done work (shown at first, as on the website).
29367
29468
  */
29368
29469
  function RoadmapView({ active, width, height, go }) {
29369
29470
  const { today } = useDeps();
@@ -29375,6 +29476,7 @@ function RoadmapView({ active, width, height, go }) {
29375
29476
  const [zoom, setZoom] = useState("half");
29376
29477
  const [shift, setShift] = useState(0);
29377
29478
  const [at, setAt] = useState(0);
29479
+ const [hideDone, setHideDone] = useState(false);
29378
29480
  const portfolio = portfolios[Math.min(pf, portfolios.length - 1)];
29379
29481
  const day = today();
29380
29482
  const w = useMemo(() => roadmapWindow(zoom, day, shift), [
@@ -29388,14 +29490,20 @@ function RoadmapView({ active, width, height, go }) {
29388
29490
  milestones
29389
29491
  }, portfolio.id, {
29390
29492
  health: "all",
29391
- ownerId: null
29493
+ ownerId: null,
29494
+ hideDone
29392
29495
  }, /* @__PURE__ */ new Set(), day) : [], [
29393
29496
  portfolio,
29394
29497
  programs,
29395
29498
  projects,
29396
29499
  milestones,
29397
- day
29500
+ day,
29501
+ hideDone
29398
29502
  ]);
29503
+ const doneCount = portfolio ? roadmapDoneCount({
29504
+ programs,
29505
+ projects
29506
+ }, portfolio.id) : 0;
29399
29507
  const row = rows[Math.min(at, rows.length - 1)];
29400
29508
  useInput((input, key) => {
29401
29509
  if (input === "[") setPf((i) => (i - 1 + portfolios.length) % Math.max(1, portfolios.length));
@@ -29404,8 +29512,11 @@ function RoadmapView({ active, width, height, go }) {
29404
29512
  else if (key.leftArrow || input === "h") setShift((s) => s - 1);
29405
29513
  else if (key.rightArrow || input === "l") setShift((s) => s + 1);
29406
29514
  else if (input === "t") setShift(0);
29407
- else if (key.upArrow || input === "k") setAt((i) => step(i, -1, rows.length));
29408
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, rows.length));
29515
+ else if (input === "D" && (doneCount > 0 || hideDone)) {
29516
+ setHideDone((v) => !v);
29517
+ setAt(0);
29518
+ } else if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, rows.length));
29519
+ else if (key.downArrow || input === "j") setAt((i) => step$1(i, 1, rows.length));
29409
29520
  else if (key.return && row) {
29410
29521
  if (row.kind === "project") go.openProject(row.id);
29411
29522
  else go.openProgram(row.id);
@@ -29421,7 +29532,7 @@ function RoadmapView({ active, width, height, go }) {
29421
29532
  paddingX: 1,
29422
29533
  children: [
29423
29534
  /* @__PURE__ */ jsxs(Heading, {
29424
- right: `${label} · [ ] portfolio · z zoom · ←→ time · t today`,
29535
+ right: `${label}${doneCount ? ` · ${doneCount} done ${hideDone ? "hidden" : "shown"} (D)` : ""} · [ ] portfolio · z zoom · ←→ time · t today`,
29425
29536
  children: ["Roadmap: ", portfolio?.name ?? "no portfolios yet"]
29426
29537
  }),
29427
29538
  /* @__PURE__ */ jsxs(Text, {
@@ -29430,7 +29541,7 @@ function RoadmapView({ active, width, height, go }) {
29430
29541
  }),
29431
29542
  rows.length === 0 ? /* @__PURE__ */ jsx(Text, {
29432
29543
  color: theme.muted,
29433
- children: "Nothing scheduled in this portfolio."
29544
+ children: hideDone && doneCount ? "Everything in this portfolio is done. D shows it." : "Nothing scheduled in this portfolio."
29434
29545
  }) : null,
29435
29546
  view.items.map((r, i) => {
29436
29547
  const on = active && view.start + i === at;
@@ -30148,8 +30259,8 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
30148
30259
  useInput((input, key) => {
30149
30260
  if (!task) return onClose();
30150
30261
  if (key.escape) return onClose();
30151
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, rows));
30152
- if (key.downArrow || input === "j") return setAt((i) => step(i, 1, rows));
30262
+ if (key.upArrow || input === "k") return setAt((i) => step$1(i, -1, rows));
30263
+ if (key.downArrow || input === "j") return setAt((i) => step$1(i, 1, rows));
30153
30264
  if (input === "c") return setEditing({ kind: "comment" });
30154
30265
  if (input === "a") return setEditing({ kind: "step" });
30155
30266
  if (input === "C") {
@@ -30171,6 +30282,13 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
30171
30282
  const item = task.checklist[at - stepsFrom];
30172
30283
  if (item && (input === " " || key.return)) return store.getState().toggleChecklistItem(task.id, item.id);
30173
30284
  if (item && input === "-") return store.getState().removeChecklistItem(task.id, item.id);
30285
+ if (item && input === "e") {
30286
+ if (viewOnly) return go.notify("You can only view this harbor.");
30287
+ return setEditing({
30288
+ kind: "restep",
30289
+ id: item.id
30290
+ });
30291
+ }
30174
30292
  if (input === "o") {
30175
30293
  if (viewOnly) return go.notify("You can only view this harbor.");
30176
30294
  if (alsoOnIds(task).length === 0 && alsoOnChoices(task, people).length === 0) return go.notify("Nobody else to put on it yet. Invite your crew first.");
@@ -30239,7 +30357,18 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
30239
30357
  done();
30240
30358
  }
30241
30359
  });
30242
- else if (editing?.kind === "mine") editor = /* @__PURE__ */ jsx(Picker, {
30360
+ else if (editing?.kind === "restep") {
30361
+ const item = task.checklist.find((c) => c.id === editing.id);
30362
+ editor = /* @__PURE__ */ jsx(TextField, {
30363
+ label: "Checklist step",
30364
+ initial: item?.title ?? "",
30365
+ onCancel: done,
30366
+ onSubmit: (text) => {
30367
+ if (item) store.getState().renameChecklistItem(task.id, item.id, text);
30368
+ done();
30369
+ }
30370
+ });
30371
+ } else if (editing?.kind === "mine") editor = /* @__PURE__ */ jsx(Picker, {
30243
30372
  title: "One of your comments: Enter to change it or take it out",
30244
30373
  choices: mine.map((c) => ({
30245
30374
  label: c.body.replace(/\s+/g, " "),
@@ -30562,7 +30691,7 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
30562
30691
  editor ?? /* @__PURE__ */ jsx(Text, {
30563
30692
  color: theme.muted,
30564
30693
  wrap: "truncate-end",
30565
- children: "↑↓ field · Enter change · o who's on it · c comment · C yours · a add step · space tick · x done · S sounding · d delete · Esc close"
30694
+ children: "↑↓ field · Enter change · o who's on it · c comment · C yours · a add step · e edit step · space tick · x done · S sounding · d delete · Esc close"
30566
30695
  })
30567
30696
  ]
30568
30697
  });
@@ -30628,8 +30757,8 @@ function WaitsEditor({ task, onDone }) {
30628
30757
  const set = (ids) => store.getState().updateTask(task.id, blockersPatch(ids));
30629
30758
  useInput((input, key) => {
30630
30759
  if (key.escape) return onDone();
30631
- if (key.upArrow) return setAt((i) => step(i, -1, list.length));
30632
- if (key.downArrow) return setAt((i) => step(i, 1, list.length));
30760
+ if (key.upArrow) return setAt((i) => step$1(i, -1, list.length));
30761
+ if (key.downArrow) return setAt((i) => step$1(i, 1, list.length));
30633
30762
  if (key.return) {
30634
30763
  const pick = list[Math.min(at, list.length - 1)];
30635
30764
  if (!pick) return onDone();
@@ -30738,10 +30867,10 @@ function TodayView({ active, width, height, go }) {
30738
30867
  const selected = mine[Math.min(at, mine.length - 1)];
30739
30868
  const late = mine.filter((t) => isLate(t, day)).length;
30740
30869
  useInput((input, key) => {
30741
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, mine.length));
30742
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, mine.length));
30743
- else if (key.pageDown) setAt((i) => step(i, 10, mine.length));
30744
- else if (key.pageUp) setAt((i) => step(i, -10, mine.length));
30870
+ if (key.upArrow || input === "k") setAt((i) => step$1(i, -1, mine.length));
30871
+ else if (key.downArrow || input === "j") setAt((i) => step$1(i, 1, mine.length));
30872
+ else if (key.pageDown) setAt((i) => step$1(i, 10, mine.length));
30873
+ else if (key.pageUp) setAt((i) => step$1(i, -10, mine.length));
30745
30874
  else if (key.return && selected) go.openTask(selected.id);
30746
30875
  else if (input === "x" && selected) moveTask(store, go, selected, "done");
30747
30876
  else if (input === "n" && projects.length === 0) go.create("project");
@@ -30936,8 +31065,8 @@ function WeekView({ active, width, height, go }) {
30936
31065
  if (input === "t") return goWeek(0);
30937
31066
  if (input === "v") return setReview((v) => !v);
30938
31067
  if (review) return;
30939
- if (key.upArrow || input === "k") setAt(step(sel, -1, picks.length));
30940
- else if (key.downArrow || input === "j") setAt(step(sel, 1, picks.length));
31068
+ if (key.upArrow || input === "k") setAt(step$1(sel, -1, picks.length));
31069
+ else if (key.downArrow || input === "j") setAt(step$1(sel, 1, picks.length));
30941
31070
  else if (key.return && task) go.openTask(task.id);
30942
31071
  else if (input === "p" && line?.kind === "open") go.pick(`Plan “${line.task.title}” on`, dayChoices(), (d) => {
30943
31072
  if (store.getState().planTask(line.task.id, d)) go.notify(`On ${dayName(d)}. h changes its hours.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.82.0",
3
+ "version": "0.84.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",