@harborppm/cli 0.83.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 +194 -96
  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,
@@ -23937,15 +24022,15 @@ function tasksSlice({ set, get, write }) {
23937
24022
  github: withRef(t.github, ref)
23938
24023
  } : t) });
23939
24024
  },
23940
- readPing: (id) => {
24025
+ readPing: (mark) => {
23941
24026
  const ids = get().readPingIds ?? [];
23942
- if (ids.includes(id)) return;
23943
- set({ readPingIds: [...ids, id] });
24027
+ if (ids.includes(mark)) return;
24028
+ set({ readPingIds: [...ids, mark] });
23944
24029
  },
23945
- catchUp: (ids) => {
23946
- if (ids.length === 0) return;
24030
+ catchUp: (marks) => {
24031
+ if (marks.length === 0) return;
23947
24032
  const have = new Set(get().readPingIds ?? []);
23948
- for (const id of ids) have.add(id);
24033
+ for (const mark of marks) have.add(mark);
23949
24034
  set({ readPingIds: [...have] });
23950
24035
  },
23951
24036
  tickVoyage: (ids) => {
@@ -24014,7 +24099,7 @@ function tasksSlice({ set, get, write }) {
24014
24099
  })));
24015
24100
  const final = stampAssignment(row, automated, me, at);
24016
24101
  if (final.status === "done" && row.status !== "done") {
24017
- const follow = nextOccurrence(final, uid("t"), todayISO(), at);
24102
+ const follow = nextOccurrence(final, uid("t"), todayISO(), at, [...tasksOf(s.tasks, row.projectId), ...follows]);
24018
24103
  if (follow) follows.push(follow);
24019
24104
  }
24020
24105
  return final;
@@ -24033,7 +24118,8 @@ function tasksSlice({ set, get, write }) {
24033
24118
  toggleFocus: (taskId) => {
24034
24119
  const personId = get().currentUserId;
24035
24120
  const date = todayISO();
24036
- 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));
24037
24123
  if (ids.includes(taskId)) {
24038
24124
  set({ ropes: upsertFocus(get().ropes, personId, date, ids.filter((id) => id !== taskId)) });
24039
24125
  return "removed";
@@ -24133,6 +24219,7 @@ var init_tasks = __esmMin((() => {
24133
24219
  init_work();
24134
24220
  init_shared();
24135
24221
  init_recurrence();
24222
+ init_indexes();
24136
24223
  init_assignment();
24137
24224
  init_bulk();
24138
24225
  init_task_people();
@@ -27330,8 +27417,8 @@ function DockPanel({ id, width, height, go, active, onClose }) {
27330
27417
  useInput((input, key) => {
27331
27418
  if (key.escape) return onClose();
27332
27419
  if (!project) return;
27333
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, changes.length));
27334
- 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));
27335
27422
  if (input === "a" || input === "n") return newChange();
27336
27423
  if (input === "d" && changes.length) {
27337
27424
  const i = Math.min(at, changes.length - 1);
@@ -27509,8 +27596,8 @@ function LifeboatsPanel({ id, width, height, go, active, onClose }) {
27509
27596
  };
27510
27597
  useInput((input, key) => {
27511
27598
  if (key.escape) return onClose();
27512
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, lines.length));
27513
- 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));
27514
27601
  if (key.return && line) return choose(line);
27515
27602
  }, { isActive: active !== false });
27516
27603
  const rows = Math.max(1, Math.floor((height - 6 - notes.length) / 3));
@@ -27642,8 +27729,8 @@ function ShorePanel({ id, width, height, go, active, onClose }) {
27642
27729
  useInput((input, key) => {
27643
27730
  if (key.escape) return onClose();
27644
27731
  if (!answer) return;
27645
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, SHORE_KINDS.length));
27646
- 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));
27647
27734
  if (viewOnly) return;
27648
27735
  if (input === "p") return go.ask("The channel's incoming-webhook address", (text) => {
27649
27736
  const url = text.trim();
@@ -27802,8 +27889,8 @@ function WelcomeView({ when, active, width, height, go, onDone }) {
27802
27889
  const look = w.lookFirst;
27803
27890
  useInput((input, key) => {
27804
27891
  if (key.escape || input === "g") return onDone();
27805
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, look.length));
27806
- 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));
27807
27894
  else if (key.return) {
27808
27895
  const l = look[at];
27809
27896
  if (!l) return onDone();
@@ -28212,11 +28299,11 @@ function ProgramView({ programId, active, width, height, go, onBack }) {
28212
28299
  return setShowDone((v) => !v);
28213
28300
  }
28214
28301
  if (side === "projects") {
28215
- if (key.upArrow || input === "k") setPAt((i) => step(i, -1, mine.length));
28216
- 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));
28217
28304
  else if (key.return && project) go.openProject(project.id);
28218
- } else if (key.upArrow || input === "k") setMAt((i) => step(i, -1, marks.length));
28219
- 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));
28220
28307
  else if (key.return && mark) go.openPanel("milestone", mark.id);
28221
28308
  else if (input === " " && mark) store.getState().toggleMilestone(mark.id);
28222
28309
  else if (input === "d" && mark) go.confirm(`Delete the milestone “${mark.name}”? u undoes it.`, () => store.getState().deleteMilestone(mark.id), "Delete");
@@ -28440,8 +28527,8 @@ function PortfolioView({ portfolioId, active, width, height, go, onBack }) {
28440
28527
  if (input === "n") return go.create("program", { portfolioId });
28441
28528
  if (input === "v") return setLayout((l) => l === "board" ? "bands" : "board");
28442
28529
  if (input === "D" && split.done.length > 0) return setShowDone((v) => !v);
28443
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, list.length));
28444
- 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));
28445
28532
  else if (key.return && program) go.openProgram(program.id);
28446
28533
  }, { isActive: active });
28447
28534
  if (!portfolio) return /* @__PURE__ */ jsx(Text, {
@@ -28830,8 +28917,8 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
28830
28917
  if (input === "v") return setMode((m) => m === "board" ? "list" : m === "list" ? "budget" : m === "budget" ? "files" : "board");
28831
28918
  if (mode === "files") {
28832
28919
  const f = files[Math.min(fileAt, files.length - 1)];
28833
- if (key.upArrow || input === "k") setFileAt((i) => step(i, -1, files.length));
28834
- 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));
28835
28922
  else if (key.return && f) go.openTask(f.task.id);
28836
28923
  return;
28837
28924
  }
@@ -28875,8 +28962,8 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
28875
28962
  if (mode === "budget") {
28876
28963
  const entries = project ? budgetOf({ spendEntries }, project, /* @__PURE__ */ new Date()).entries : [];
28877
28964
  const entry = entries[Math.min(spendAt, entries.length - 1)];
28878
- if (key.upArrow || input === "k") setSpendAt((i) => step(i, -1, entries.length));
28879
- 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));
28880
28967
  else if (input === "a" || input === "n") go.create("spend", { projectId });
28881
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");
28882
28969
  return;
@@ -28892,20 +28979,20 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
28892
28979
  if (input === "x" && selected) return moveTask(store, go, selected, "done");
28893
28980
  if (input === "d" && selected) return go.confirm(`Delete ${keyOf$1({ projects }, selected)} “${selected.title}”? u undoes it.`, () => store.getState().deleteTask(selected.id), "Delete");
28894
28981
  if (mode === "board") {
28895
- if (key.leftArrow || input === "h") setCol((c) => step(c, -1, columns.length));
28896
- 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));
28897
28984
  else if (key.upArrow || input === "k") setRowByCol((r) => ({
28898
28985
  ...r,
28899
- [col]: step(row, -1, column?.tasks.length ?? 0)
28986
+ [col]: step$1(row, -1, column?.tasks.length ?? 0)
28900
28987
  }));
28901
28988
  else if (key.downArrow || input === "j") setRowByCol((r) => ({
28902
28989
  ...r,
28903
- [col]: step(row, 1, column?.tasks.length ?? 0)
28990
+ [col]: step$1(row, 1, column?.tasks.length ?? 0)
28904
28991
  }));
28905
- } else if (key.upArrow || input === "k") setListAt((i) => step(i, -1, list.length));
28906
- else if (key.downArrow || input === "j") setListAt((i) => step(i, 1, list.length));
28907
- else if (key.pageDown) setListAt((i) => step(i, 10, list.length));
28908
- 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));
28909
28996
  }, { isActive: active });
28910
28997
  if (!project) return /* @__PURE__ */ jsx(Box, {
28911
28998
  paddingX: 1,
@@ -29376,7 +29463,8 @@ const NAME = 26;
29376
29463
  /**
29377
29464
  * A portfolio's programs and projects across the months, as on the
29378
29465
  * website's roadmap: bars in each one's health, ◆ for milestones. [ ] for
29379
- * 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).
29380
29468
  */
29381
29469
  function RoadmapView({ active, width, height, go }) {
29382
29470
  const { today } = useDeps();
@@ -29388,6 +29476,7 @@ function RoadmapView({ active, width, height, go }) {
29388
29476
  const [zoom, setZoom] = useState("half");
29389
29477
  const [shift, setShift] = useState(0);
29390
29478
  const [at, setAt] = useState(0);
29479
+ const [hideDone, setHideDone] = useState(false);
29391
29480
  const portfolio = portfolios[Math.min(pf, portfolios.length - 1)];
29392
29481
  const day = today();
29393
29482
  const w = useMemo(() => roadmapWindow(zoom, day, shift), [
@@ -29401,14 +29490,20 @@ function RoadmapView({ active, width, height, go }) {
29401
29490
  milestones
29402
29491
  }, portfolio.id, {
29403
29492
  health: "all",
29404
- ownerId: null
29493
+ ownerId: null,
29494
+ hideDone
29405
29495
  }, /* @__PURE__ */ new Set(), day) : [], [
29406
29496
  portfolio,
29407
29497
  programs,
29408
29498
  projects,
29409
29499
  milestones,
29410
- day
29500
+ day,
29501
+ hideDone
29411
29502
  ]);
29503
+ const doneCount = portfolio ? roadmapDoneCount({
29504
+ programs,
29505
+ projects
29506
+ }, portfolio.id) : 0;
29412
29507
  const row = rows[Math.min(at, rows.length - 1)];
29413
29508
  useInput((input, key) => {
29414
29509
  if (input === "[") setPf((i) => (i - 1 + portfolios.length) % Math.max(1, portfolios.length));
@@ -29417,8 +29512,11 @@ function RoadmapView({ active, width, height, go }) {
29417
29512
  else if (key.leftArrow || input === "h") setShift((s) => s - 1);
29418
29513
  else if (key.rightArrow || input === "l") setShift((s) => s + 1);
29419
29514
  else if (input === "t") setShift(0);
29420
- else if (key.upArrow || input === "k") setAt((i) => step(i, -1, rows.length));
29421
- 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));
29422
29520
  else if (key.return && row) {
29423
29521
  if (row.kind === "project") go.openProject(row.id);
29424
29522
  else go.openProgram(row.id);
@@ -29434,7 +29532,7 @@ function RoadmapView({ active, width, height, go }) {
29434
29532
  paddingX: 1,
29435
29533
  children: [
29436
29534
  /* @__PURE__ */ jsxs(Heading, {
29437
- right: `${label} · [ ] portfolio · z zoom · ←→ time · t today`,
29535
+ right: `${label}${doneCount ? ` · ${doneCount} done ${hideDone ? "hidden" : "shown"} (D)` : ""} · [ ] portfolio · z zoom · ←→ time · t today`,
29438
29536
  children: ["Roadmap: ", portfolio?.name ?? "no portfolios yet"]
29439
29537
  }),
29440
29538
  /* @__PURE__ */ jsxs(Text, {
@@ -29443,7 +29541,7 @@ function RoadmapView({ active, width, height, go }) {
29443
29541
  }),
29444
29542
  rows.length === 0 ? /* @__PURE__ */ jsx(Text, {
29445
29543
  color: theme.muted,
29446
- children: "Nothing scheduled in this portfolio."
29544
+ children: hideDone && doneCount ? "Everything in this portfolio is done. D shows it." : "Nothing scheduled in this portfolio."
29447
29545
  }) : null,
29448
29546
  view.items.map((r, i) => {
29449
29547
  const on = active && view.start + i === at;
@@ -30161,8 +30259,8 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
30161
30259
  useInput((input, key) => {
30162
30260
  if (!task) return onClose();
30163
30261
  if (key.escape) return onClose();
30164
- if (key.upArrow || input === "k") return setAt((i) => step(i, -1, rows));
30165
- 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));
30166
30264
  if (input === "c") return setEditing({ kind: "comment" });
30167
30265
  if (input === "a") return setEditing({ kind: "step" });
30168
30266
  if (input === "C") {
@@ -30659,8 +30757,8 @@ function WaitsEditor({ task, onDone }) {
30659
30757
  const set = (ids) => store.getState().updateTask(task.id, blockersPatch(ids));
30660
30758
  useInput((input, key) => {
30661
30759
  if (key.escape) return onDone();
30662
- if (key.upArrow) return setAt((i) => step(i, -1, list.length));
30663
- 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));
30664
30762
  if (key.return) {
30665
30763
  const pick = list[Math.min(at, list.length - 1)];
30666
30764
  if (!pick) return onDone();
@@ -30769,10 +30867,10 @@ function TodayView({ active, width, height, go }) {
30769
30867
  const selected = mine[Math.min(at, mine.length - 1)];
30770
30868
  const late = mine.filter((t) => isLate(t, day)).length;
30771
30869
  useInput((input, key) => {
30772
- if (key.upArrow || input === "k") setAt((i) => step(i, -1, mine.length));
30773
- else if (key.downArrow || input === "j") setAt((i) => step(i, 1, mine.length));
30774
- else if (key.pageDown) setAt((i) => step(i, 10, mine.length));
30775
- 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));
30776
30874
  else if (key.return && selected) go.openTask(selected.id);
30777
30875
  else if (input === "x" && selected) moveTask(store, go, selected, "done");
30778
30876
  else if (input === "n" && projects.length === 0) go.create("project");
@@ -30967,8 +31065,8 @@ function WeekView({ active, width, height, go }) {
30967
31065
  if (input === "t") return goWeek(0);
30968
31066
  if (input === "v") return setReview((v) => !v);
30969
31067
  if (review) return;
30970
- if (key.upArrow || input === "k") setAt(step(sel, -1, picks.length));
30971
- 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));
30972
31070
  else if (key.return && task) go.openTask(task.id);
30973
31071
  else if (input === "p" && line?.kind === "open") go.pick(`Plan “${line.task.title}” on`, dayChoices(), (d) => {
30974
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.83.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",