daily-focus 1.2.0-dev.5 → 1.2.0-dev.8

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.
package/public/app.js CHANGED
@@ -1013,9 +1013,6 @@ function subscribe(onState, onError) {
1013
1013
  function postTimeZone(timeZone) {
1014
1014
  return post("/api/timezone", { timeZone });
1015
1015
  }
1016
- function postSession(body) {
1017
- return post("/api/session", body);
1018
- }
1019
1016
  function postBoardRefresh() {
1020
1017
  return post("/api/board/refresh");
1021
1018
  }
@@ -1080,9 +1077,7 @@ var VIEWS = ["today", "board", "tickets", "settings"];
1080
1077
  var RUN_PREFIX = "run:";
1081
1078
  var PANEL_PREFIX = "panel:";
1082
1079
  var VIEW_KEY = "daily-focus:view";
1083
- var UNATTENDED_SEEN_KEY = "daily-focus:unattended-seen";
1084
1080
  var AGENT_RUN_SEEN_KEY = "daily-focus:agent-run-seen";
1085
- var FOCUS_MODE_KEY = "daily-focus:focus-mode";
1086
1081
  var DETAIL_KEY = "daily-focus:detail-for";
1087
1082
  var state = y2(null);
1088
1083
  function createUi(initial = {}) {
@@ -1098,15 +1093,12 @@ function createUi(initial = {}) {
1098
1093
  focusField: y2(initial.focusField ?? null),
1099
1094
  openDrawers: y2(initial.openDrawers ?? /* @__PURE__ */ new Set()),
1100
1095
  ticketMode: y2(initial.ticketMode ?? "sync"),
1101
- unattendedSeen: y2(initial.unattendedSeen ?? null),
1102
1096
  agentReportSeen: y2(initial.agentReportSeen ?? null),
1103
1097
  messageOpen: y2(initial.messageOpen ?? /* @__PURE__ */ new Map()),
1104
1098
  view: y2(initial.view ?? "today"),
1105
- focusMode: y2(initial.focusMode ?? false),
1106
1099
  theme: y2(initial.theme ?? "system"),
1107
1100
  toast: y2(initial.toast ?? null),
1108
1101
  connectionError: y2(initial.connectionError ?? null),
1109
- clock: y2(initial.clock ?? Date.now()),
1110
1102
  settingsReturn: y2(initial.settingsReturn ?? "today"),
1111
1103
  settings: y2(initial.settings ?? null),
1112
1104
  settingsDraft: y2(initial.settingsDraft ?? /* @__PURE__ */ new Map()),
@@ -1125,9 +1117,7 @@ function remembered() {
1125
1117
  return {
1126
1118
  // Settings is a page to visit, so a reload returns to the view underneath.
1127
1119
  view: isView(view) && view !== "settings" ? view : "today",
1128
- unattendedSeen: localStorage.getItem(UNATTENDED_SEEN_KEY),
1129
1120
  agentReportSeen: localStorage.getItem(AGENT_RUN_SEEN_KEY),
1130
- focusMode: localStorage.getItem(FOCUS_MODE_KEY) === "1",
1131
1121
  detailFor: sessionStorage.getItem(DETAIL_KEY)
1132
1122
  };
1133
1123
  } catch {
@@ -1295,208 +1285,6 @@ function ThemeToggle({ ui: ui2, handlers: handlers2 }) {
1295
1285
  );
1296
1286
  }
1297
1287
 
1298
- // client/format.ts
1299
- var DAY_FORMAT = {
1300
- weekday: "long",
1301
- day: "numeric",
1302
- month: "long"
1303
- };
1304
- function formatters(timeZone) {
1305
- return {
1306
- time: new Intl.DateTimeFormat(void 0, { timeZone, hour: "2-digit", minute: "2-digit" }),
1307
- day: new Intl.DateTimeFormat(void 0, { timeZone, ...DAY_FORMAT }),
1308
- shortDay: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short" }),
1309
- shortDayYear: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short", year: "numeric" }),
1310
- weekday: new Intl.DateTimeFormat(void 0, { timeZone, weekday: "long" })
1311
- };
1312
- }
1313
- var clockFormats = formatters();
1314
- var dateFormats = formatters("UTC");
1315
- var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
1316
- function formatted(value, kind) {
1317
- const bare = typeof value === "string" && DATE_ONLY.test(value);
1318
- const d4 = bare ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : parseDate(value);
1319
- return d4 && !Number.isNaN(d4.getTime()) ? (bare ? dateFormats : clockFormats)[kind].format(d4) : "";
1320
- }
1321
- function parseDate(value) {
1322
- if (typeof value !== "string" || value === "") return null;
1323
- const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
1324
- if (dateOnly) return new Date(+dateOnly[1], +dateOnly[2] - 1, +dateOnly[3]);
1325
- const d4 = new Date(value);
1326
- return Number.isNaN(d4.getTime()) ? null : d4;
1327
- }
1328
- function formatTime(value) {
1329
- return formatted(value, "time");
1330
- }
1331
- function formatDay(value) {
1332
- return formatted(value, "day");
1333
- }
1334
- function formatShortDay(value) {
1335
- return formatted(value, "shortDay");
1336
- }
1337
- function formatWeekday(value) {
1338
- return formatted(value, "weekday");
1339
- }
1340
- var dayKeyFmt = null;
1341
- var calendarZone;
1342
- function setCalendarZone(timeZone) {
1343
- if (timeZone === calendarZone) return;
1344
- calendarZone = timeZone;
1345
- try {
1346
- clockFormats = formatters(timeZone);
1347
- dayKeyFmt = timeZone ? new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }) : null;
1348
- } catch {
1349
- dayKeyFmt = null;
1350
- clockFormats = formatters();
1351
- }
1352
- }
1353
- function dayKey(d4) {
1354
- if (dayKeyFmt) {
1355
- const parts = dayKeyFmt.formatToParts(d4);
1356
- const part = (type) => parts.find((p5) => p5.type === type)?.value ?? "";
1357
- return `${part("year")}-${part("month")}-${part("day")}`;
1358
- }
1359
- const m4 = String(d4.getMonth() + 1).padStart(2, "0");
1360
- const day = String(d4.getDate()).padStart(2, "0");
1361
- return `${d4.getFullYear()}-${m4}-${day}`;
1362
- }
1363
- function splitKey(key) {
1364
- const [y4, m4, d4] = key.split("-").map(Number);
1365
- return [y4, m4 - 1, d4];
1366
- }
1367
- function keyAt(ms) {
1368
- return new Date(ms).toISOString().slice(0, 10);
1369
- }
1370
- function addDays(key, days) {
1371
- const [y4, m4, d4] = splitKey(key);
1372
- return keyAt(Date.UTC(y4, m4, d4 + days));
1373
- }
1374
- function addMonths(key, months) {
1375
- const [y4, m4, d4] = splitKey(key);
1376
- const lastDay = new Date(Date.UTC(y4, m4 + months + 1, 0)).getUTCDate();
1377
- return keyAt(Date.UTC(y4, m4 + months, Math.min(d4, lastDay)));
1378
- }
1379
- function daysFromToday(value, now = /* @__PURE__ */ new Date()) {
1380
- if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
1381
- const [y4, m4, d4] = splitKey(value);
1382
- const [ty, tm, td] = splitKey(dayKey(now));
1383
- return Math.round((Date.UTC(y4, m4, d4) - Date.UTC(ty, tm, td)) / 864e5);
1384
- }
1385
- const target = parseDate(value);
1386
- if (!target) return null;
1387
- const a4 = Date.UTC(...splitKey(dayKey(now)));
1388
- const b4 = Date.UTC(...splitKey(dayKey(target)));
1389
- return Math.round((b4 - a4) / 864e5);
1390
- }
1391
- function relativeDay(value, now = /* @__PURE__ */ new Date()) {
1392
- const diff = daysFromToday(value, now);
1393
- if (diff === null) return "";
1394
- if (diff === 0) return "today";
1395
- if (diff === 1) return "tomorrow";
1396
- if (diff === -1) return "yesterday";
1397
- if (diff > 1 && diff <= 7) return `in ${diff} days`;
1398
- if (diff < -1 && diff >= -7) return `${Math.abs(diff)} days ago`;
1399
- return formatShortDay(value);
1400
- }
1401
- function formatDuration(minutes) {
1402
- const h5 = Math.floor(minutes / 60);
1403
- const m4 = minutes % 60;
1404
- if (h5 === 0) return `${m4} min`;
1405
- if (m4 === 0) return `${h5} h`;
1406
- return `${h5} h ${m4} min`;
1407
- }
1408
- function relativeTime(value, now = /* @__PURE__ */ new Date()) {
1409
- const d4 = parseDate(value);
1410
- if (!d4) return "";
1411
- const seconds = Math.round((now.getTime() - d4.getTime()) / 1e3);
1412
- if (seconds < 60) return "just now";
1413
- const minutes = Math.round(seconds / 60);
1414
- if (minutes < 60) return `${minutes} min ago`;
1415
- const hours = Math.round(minutes / 60);
1416
- if (hours < 48) return `${hours} h ago`;
1417
- const days = Math.round(hours / 24);
1418
- if (days <= 30) return `${days} days ago`;
1419
- return dayKey(d4).slice(0, 4) === dayKey(now).slice(0, 4) ? formatShortDay(value) : formatted(value, "shortDayYear");
1420
- }
1421
- function firstLine(text) {
1422
- const line = String(text).split("\n").find((l5) => l5.trim() !== "") ?? "";
1423
- return line.replace(/^[#>*\-\s]+/, "").replace(/[*_`]/g, "").trim();
1424
- }
1425
-
1426
- // client/timer.ts
1427
- function clock(seconds) {
1428
- const total = Math.abs(seconds);
1429
- const mm = Math.floor(total / 60);
1430
- const ss = Math.floor(total % 60);
1431
- return `${seconds < 0 ? "+" : ""}${mm}:${String(ss).padStart(2, "0")}`;
1432
- }
1433
- function remainingSeconds(active, nowMs) {
1434
- return Math.round((new Date(active.endsAt).getTime() - nowMs) / 1e3);
1435
- }
1436
- function unattendedNotice(closed, handlers2) {
1437
- const stoppedAt = formatTime(closed.endedAt);
1438
- const headline = closed.reason === "away" ? `Timer stopped at ${stoppedAt} \u2014 nothing had touched the machine since.` : `Timer stopped at ${stoppedAt} \u2014 it had been left running.`;
1439
- return el(
1440
- "div",
1441
- { class: "timer__away", role: "status" },
1442
- el(
1443
- "span",
1444
- { class: "timer__away-body" },
1445
- el("span", { class: "timer__away-text" }, headline),
1446
- el(
1447
- "span",
1448
- { class: "timer__away-detail" },
1449
- `${formatDuration(closed.actualMinutes)} logged on \u201C${closed.title}\u201D. `,
1450
- closed.reason === "away" ? "The time you were away is not in the record." : "Treat that as an upper bound, not a measurement."
1451
- )
1452
- ),
1453
- el("button", { type: "button", class: "button", onClick: () => handlers2.startSession(closed.id) }, "Pick it back up"),
1454
- el(
1455
- "button",
1456
- {
1457
- type: "button",
1458
- class: "icon-button",
1459
- "aria-label": "Dismiss",
1460
- onClick: () => handlers2.dismissUnattended(closed.endedAt)
1461
- },
1462
- "\u2715"
1463
- )
1464
- );
1465
- }
1466
- function Timer({ state: state2, ui: ui2, handlers: handlers2 }) {
1467
- const { active, completedToday, minutesToday, unattendedClose } = state2.session;
1468
- const closed = unattendedClose && ui2.unattendedSeen.value !== unattendedClose.endedAt ? unattendedClose : null;
1469
- if (!active && completedToday === 0 && !closed) {
1470
- return el("div", { class: "timer", id: "timer", hidden: true });
1471
- }
1472
- const tally = completedToday > 0 ? `${completedToday} session${completedToday === 1 ? "" : "s"} today \xB7 ${formatDuration(minutesToday)}` : null;
1473
- if (!active) {
1474
- return el(
1475
- "div",
1476
- { class: "timer", id: "timer", hidden: false, "data-state": closed ? "stopped" : "idle" },
1477
- closed ? unattendedNotice(closed, handlers2) : null,
1478
- tally ? el("span", { class: "timer__tally" }, tally) : null
1479
- );
1480
- }
1481
- const remaining = remainingSeconds(active, ui2.clock.value);
1482
- const overrun = remaining < 0;
1483
- return el(
1484
- "div",
1485
- { class: "timer", id: "timer", hidden: false, "data-state": overrun ? "overrun" : "running" },
1486
- el("span", { class: "timer__clock" }, clock(remaining)),
1487
- el(
1488
- "span",
1489
- { class: "timer__title" },
1490
- active.title,
1491
- overrun ? el("span", { class: "timer__note" }, ` \xB7 ${active.minutes} min in, still counting`) : null,
1492
- // The one thing a kitchen timer can't tell you.
1493
- active.collidesWithNextEvent && !overrun ? el("span", { class: "timer__note timer__note--warn" }, ` \xB7 runs into ${formatTime(active.nextEventAt)}`) : null
1494
- ),
1495
- tally ? el("span", { class: "timer__tally" }, tally) : null,
1496
- el("button", { type: "button", class: "button", onClick: () => handlers2.stopSession() }, "Stop")
1497
- );
1498
- }
1499
-
1500
1288
  // client/actions.ts
1501
1289
  function afterRender(fn) {
1502
1290
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(fn);
@@ -1550,7 +1338,6 @@ function adoptState(next) {
1550
1338
  state.value = next;
1551
1339
  ui.connectionError.value = null;
1552
1340
  });
1553
- startLocalTick();
1554
1341
  checkStore(next.storeId);
1555
1342
  checkAssetVersion(next.assetVersion);
1556
1343
  }
@@ -1593,7 +1380,8 @@ var PAST_TENSE = {
1593
1380
  dismiss: "Dismissed",
1594
1381
  snooze: "Snoozed",
1595
1382
  reopen: "Restored",
1596
- note: "Note saved"
1383
+ note: "Note saved",
1384
+ nudge: "Nudged, back tomorrow"
1597
1385
  };
1598
1386
  var lastAction = null;
1599
1387
  var parkedFrom = /* @__PURE__ */ new Map();
@@ -1605,6 +1393,9 @@ function unpark(id) {
1605
1393
  function undoLast() {
1606
1394
  if (lastAction && lastAction.action !== "reopen") void unpark(lastAction.id);
1607
1395
  }
1396
+ function isRemark(action) {
1397
+ return action === "note" || action === "nudge";
1398
+ }
1608
1399
  function optimistic(current, id, action, extra) {
1609
1400
  const status = action === "reopen" ? "open" : action === "dismiss" ? "dismissed" : action === "done" ? "done" : "snoozed";
1610
1401
  const statusAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1631,12 +1422,12 @@ async function applyAction(id, action, extra = {}) {
1631
1422
  if (row && action === "snooze" && item && (item.status === "done" || item.status === "dismissed")) {
1632
1423
  parkedFrom.set(id, item.status === "done" ? "done" : "dismiss");
1633
1424
  }
1634
- if (action !== "note") state.value = optimistic(current, id, action, extra);
1425
+ if (!isRemark(action)) state.value = optimistic(current, id, action, extra);
1635
1426
  });
1636
1427
  try {
1637
1428
  state.value = await postAction({ id, action, ...extra });
1638
1429
  removePending(id);
1639
- if (action === "note") {
1430
+ if (isRemark(action)) {
1640
1431
  showToast(PAST_TENSE[action]);
1641
1432
  return;
1642
1433
  }
@@ -1738,53 +1529,6 @@ async function askAgent(run, text) {
1738
1529
  showToast(`Could not ask the morning agent: ${err.message}`);
1739
1530
  }
1740
1531
  }
1741
- async function changeSession(body) {
1742
- if (body.action === "start" && "Notification" in window && Notification.permission === "default") {
1743
- void Notification.requestPermission();
1744
- }
1745
- try {
1746
- state.value = await postSession(body);
1747
- startLocalTick();
1748
- } catch (err) {
1749
- showToast(`Could not change the timer: ${err.message}`);
1750
- }
1751
- }
1752
- var tickTimer = null;
1753
- var announcedOverrunFor = null;
1754
- function startLocalTick() {
1755
- if (tickTimer) clearInterval(tickTimer);
1756
- tickTimer = setInterval(tick, 1e3);
1757
- tick();
1758
- }
1759
- function tick() {
1760
- const active = state.value?.session.active;
1761
- if (!active) {
1762
- document.title = "Daily Focus";
1763
- if (tickTimer) clearInterval(tickTimer);
1764
- tickTimer = null;
1765
- return;
1766
- }
1767
- const nowMs = Date.now();
1768
- ui.clock.value = nowMs;
1769
- const remaining = remainingSeconds(active, nowMs);
1770
- const mm = Math.floor(Math.abs(remaining) / 60);
1771
- const ss = String(Math.floor(Math.abs(remaining) % 60)).padStart(2, "0");
1772
- document.title = `${remaining < 0 ? "+" : ""}${mm}:${ss} \xB7 ${active.title}`;
1773
- if (remaining <= 0 && announcedOverrunFor !== active.startedAt) {
1774
- announcedOverrunFor = active.startedAt;
1775
- announceSessionEnd(active);
1776
- }
1777
- }
1778
- function announceSessionEnd(active) {
1779
- const label = active.minutes === 1 ? "1 minute" : `${active.minutes} minutes`;
1780
- showToast(`${label} in \u2014 still on "${active.title}"?`, null);
1781
- if ("Notification" in window && Notification.permission === "granted") {
1782
- new Notification("Still on this?", {
1783
- body: `${label} in on ${active.title}. Keep going, or stop and log it.`,
1784
- tag: "daily-focus-session"
1785
- });
1786
- }
1787
- }
1788
1532
  function showView(view) {
1789
1533
  if (ui.view.value === view) return;
1790
1534
  if (state.value && !availableViews(state.value).includes(view)) return;
@@ -1833,10 +1577,6 @@ function setTicketMode(mode) {
1833
1577
  closeMenus();
1834
1578
  });
1835
1579
  }
1836
- function setFocusMode(on) {
1837
- ui.focusMode.value = on;
1838
- remember(FOCUS_MODE_KEY, on ? "1" : null);
1839
- }
1840
1580
  function restartToast(page) {
1841
1581
  if (page.restart === "manual") showToast("Saved. Restart the dashboard to apply it.");
1842
1582
  else if (page.restart === "waiting" && state.value?.agentRun?.last?.status === "running") {
@@ -2059,15 +1799,9 @@ var handlers = {
2059
1799
  () => document.querySelector('#tickets .item[data-selected="true"]')?.scrollIntoView({ block: "center", behavior: "smooth" })
2060
1800
  );
2061
1801
  },
2062
- startSession: (id) => void changeSession({ action: "start", id }),
2063
- stopSession: () => void changeSession({ action: "stop" }),
2064
- dismissUnattended: (endedAt) => {
2065
- ui.unattendedSeen.value = endedAt;
2066
- remember(UNATTENDED_SEEN_KEY, endedAt);
2067
- },
2068
- // A nudge on Slack is invisible to GitHub, so it's recorded as a note: the agent
2069
- // reads it as free text, and the board's nudge timer starts over from it.
2070
- nudge: (id) => void applyAction(id, "note", { text: "Nudged reviewers" }),
1802
+ // A nudge on Slack is invisible to GitHub, so it's recorded: the board's nudge
1803
+ // timer starts over from it, and the agent reads that the user asked.
1804
+ nudge: (id) => void applyAction(id, "nudge"),
2071
1805
  unpark: (id) => void unpark(id),
2072
1806
  refreshBoard: () => void refreshBoard(),
2073
1807
  refreshTickets: () => void refreshTickets(),
@@ -2092,7 +1826,6 @@ var handlers = {
2092
1826
  applyTheme(mode);
2093
1827
  ui.theme.value = mode;
2094
1828
  },
2095
- setFocusMode,
2096
1829
  openHelp: () => document.getElementById("help")?.showModal(),
2097
1830
  hideToast,
2098
1831
  openSettings,
@@ -2113,6 +1846,134 @@ var handlers = {
2113
1846
  listCalendars: () => void listCalendars()
2114
1847
  };
2115
1848
 
1849
+ // client/format.ts
1850
+ var DAY_FORMAT = {
1851
+ weekday: "long",
1852
+ day: "numeric",
1853
+ month: "long"
1854
+ };
1855
+ function formatters(timeZone) {
1856
+ return {
1857
+ time: new Intl.DateTimeFormat(void 0, { timeZone, hour: "2-digit", minute: "2-digit" }),
1858
+ day: new Intl.DateTimeFormat(void 0, { timeZone, ...DAY_FORMAT }),
1859
+ shortDay: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short" }),
1860
+ shortDayYear: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short", year: "numeric" }),
1861
+ weekday: new Intl.DateTimeFormat(void 0, { timeZone, weekday: "long" })
1862
+ };
1863
+ }
1864
+ var clockFormats = formatters();
1865
+ var dateFormats = formatters("UTC");
1866
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
1867
+ function formatted(value, kind) {
1868
+ const bare = typeof value === "string" && DATE_ONLY.test(value);
1869
+ const d4 = bare ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : parseDate(value);
1870
+ return d4 && !Number.isNaN(d4.getTime()) ? (bare ? dateFormats : clockFormats)[kind].format(d4) : "";
1871
+ }
1872
+ function parseDate(value) {
1873
+ if (typeof value !== "string" || value === "") return null;
1874
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
1875
+ if (dateOnly) return new Date(+dateOnly[1], +dateOnly[2] - 1, +dateOnly[3]);
1876
+ const d4 = new Date(value);
1877
+ return Number.isNaN(d4.getTime()) ? null : d4;
1878
+ }
1879
+ function formatTime(value) {
1880
+ return formatted(value, "time");
1881
+ }
1882
+ function formatDay(value) {
1883
+ return formatted(value, "day");
1884
+ }
1885
+ function formatShortDay(value) {
1886
+ return formatted(value, "shortDay");
1887
+ }
1888
+ function formatWeekday(value) {
1889
+ return formatted(value, "weekday");
1890
+ }
1891
+ var dayKeyFmt = null;
1892
+ var calendarZone;
1893
+ function setCalendarZone(timeZone) {
1894
+ if (timeZone === calendarZone) return;
1895
+ calendarZone = timeZone;
1896
+ try {
1897
+ clockFormats = formatters(timeZone);
1898
+ dayKeyFmt = timeZone ? new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }) : null;
1899
+ } catch {
1900
+ dayKeyFmt = null;
1901
+ clockFormats = formatters();
1902
+ }
1903
+ }
1904
+ function dayKey(d4) {
1905
+ if (dayKeyFmt) {
1906
+ const parts = dayKeyFmt.formatToParts(d4);
1907
+ const part = (type) => parts.find((p5) => p5.type === type)?.value ?? "";
1908
+ return `${part("year")}-${part("month")}-${part("day")}`;
1909
+ }
1910
+ const m4 = String(d4.getMonth() + 1).padStart(2, "0");
1911
+ const day = String(d4.getDate()).padStart(2, "0");
1912
+ return `${d4.getFullYear()}-${m4}-${day}`;
1913
+ }
1914
+ function splitKey(key) {
1915
+ const [y4, m4, d4] = key.split("-").map(Number);
1916
+ return [y4, m4 - 1, d4];
1917
+ }
1918
+ function keyAt(ms) {
1919
+ return new Date(ms).toISOString().slice(0, 10);
1920
+ }
1921
+ function addDays(key, days) {
1922
+ const [y4, m4, d4] = splitKey(key);
1923
+ return keyAt(Date.UTC(y4, m4, d4 + days));
1924
+ }
1925
+ function addMonths(key, months) {
1926
+ const [y4, m4, d4] = splitKey(key);
1927
+ const lastDay = new Date(Date.UTC(y4, m4 + months + 1, 0)).getUTCDate();
1928
+ return keyAt(Date.UTC(y4, m4 + months, Math.min(d4, lastDay)));
1929
+ }
1930
+ function daysFromToday(value, now = /* @__PURE__ */ new Date()) {
1931
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
1932
+ const [y4, m4, d4] = splitKey(value);
1933
+ const [ty, tm, td] = splitKey(dayKey(now));
1934
+ return Math.round((Date.UTC(y4, m4, d4) - Date.UTC(ty, tm, td)) / 864e5);
1935
+ }
1936
+ const target = parseDate(value);
1937
+ if (!target) return null;
1938
+ const a4 = Date.UTC(...splitKey(dayKey(now)));
1939
+ const b4 = Date.UTC(...splitKey(dayKey(target)));
1940
+ return Math.round((b4 - a4) / 864e5);
1941
+ }
1942
+ function relativeDay(value, now = /* @__PURE__ */ new Date()) {
1943
+ const diff = daysFromToday(value, now);
1944
+ if (diff === null) return "";
1945
+ if (diff === 0) return "today";
1946
+ if (diff === 1) return "tomorrow";
1947
+ if (diff === -1) return "yesterday";
1948
+ if (diff > 1 && diff <= 7) return `in ${diff} days`;
1949
+ if (diff < -1 && diff >= -7) return `${Math.abs(diff)} days ago`;
1950
+ return formatShortDay(value);
1951
+ }
1952
+ function formatDuration(minutes) {
1953
+ const h5 = Math.floor(minutes / 60);
1954
+ const m4 = minutes % 60;
1955
+ if (h5 === 0) return `${m4} min`;
1956
+ if (m4 === 0) return `${h5} h`;
1957
+ return `${h5} h ${m4} min`;
1958
+ }
1959
+ function relativeTime(value, now = /* @__PURE__ */ new Date()) {
1960
+ const d4 = parseDate(value);
1961
+ if (!d4) return "";
1962
+ const seconds = Math.round((now.getTime() - d4.getTime()) / 1e3);
1963
+ if (seconds < 60) return "just now";
1964
+ const minutes = Math.round(seconds / 60);
1965
+ if (minutes < 60) return `${minutes} min ago`;
1966
+ const hours = Math.round(minutes / 60);
1967
+ if (hours < 48) return `${hours} h ago`;
1968
+ const days = Math.round(hours / 24);
1969
+ if (days <= 30) return `${days} days ago`;
1970
+ return dayKey(d4).slice(0, 4) === dayKey(now).slice(0, 4) ? formatShortDay(value) : formatted(value, "shortDayYear");
1971
+ }
1972
+ function firstLine(text) {
1973
+ const line = String(text).split("\n").find((l5) => l5.trim() !== "") ?? "";
1974
+ return line.replace(/^[#>*\-\s]+/, "").replace(/[*_`]/g, "").trim();
1975
+ }
1976
+
2116
1977
  // client/failure.ts
2117
1978
  function failureNotice(failure, { fold, className, icon, lastGood, now, onRetry }, ui2, handlers2) {
2118
1979
  return el(
@@ -2571,9 +2432,6 @@ function renderItem(item, state2, ui2, handlers2) {
2571
2432
  function ItemRow({ item, state: state2, ui: ui2, handlers: handlers2 }) {
2572
2433
  const now = new Date(state2.now);
2573
2434
  const selected = ui2.selectedId.value === item.id;
2574
- const active = state2.session?.active ?? null;
2575
- const running = active !== null && active.id === item.id;
2576
- const overrun = running && remainingSeconds(active, ui2.clock.value) < 0;
2577
2435
  const repo = githubRepo(item);
2578
2436
  const title = item.url ? el(
2579
2437
  "a",
@@ -2591,8 +2449,6 @@ function ItemRow({ item, state: state2, ui: ui2, handlers: handlers2 }) {
2591
2449
  "data-selected": String(selected),
2592
2450
  "data-priority": String(item.priority !== void 0 && item.status === "open"),
2593
2451
  "data-pending": String(ui2.pending.value.has(item.id)),
2594
- "data-running": String(running),
2595
- "data-overrun": String(overrun),
2596
2452
  "data-detail": String(ui2.detailFor.value === item.id),
2597
2453
  // A click on the card opens the panel on it: one way in, and the whole card is it.
2598
2454
  onClick: (event) => {
@@ -2612,31 +2468,20 @@ function ItemRow({ item, state: state2, ui: ui2, handlers: handlers2 }) {
2612
2468
  "div",
2613
2469
  { class: "item__body" },
2614
2470
  el("p", { class: "item__title" }, title),
2615
- renderMeta(item, state2, ui2, now),
2471
+ renderMeta(item, state2, now),
2616
2472
  item.detail ? el("p", { class: "item__detail" }, renderMarkdown(item.detail)) : null
2617
2473
  ),
2618
- renderActions(item, state2, ui2, handlers2),
2474
+ renderActions(item, ui2, handlers2),
2619
2475
  ui2.menuFor.value === item.id ? renderSnoozeMenu(item, now, handlers2) : null
2620
2476
  );
2621
2477
  }
2622
- function renderMeta(item, state2, ui2, now) {
2478
+ function renderMeta(item, state2, now) {
2623
2479
  const pills = [];
2624
2480
  const said = /* @__PURE__ */ new Set();
2625
2481
  const pill = (cls, text) => {
2626
2482
  pills.push(el("span", { class: cls }, text));
2627
2483
  said.add(text.trim().toLowerCase());
2628
2484
  };
2629
- const active = state2.session?.active ?? null;
2630
- if (active && active.id === item.id) {
2631
- pills.push(
2632
- el(
2633
- "span",
2634
- { class: "pill pill--running" },
2635
- el("span", { class: "item__running-clock" }, clock(remainingSeconds(active, ui2.clock.value))),
2636
- " focusing"
2637
- )
2638
- );
2639
- }
2640
2485
  if (item.priority !== void 0 && item.status === "open") {
2641
2486
  pill("pill pill--priority", `#${item.priority}`);
2642
2487
  }
@@ -2688,24 +2533,11 @@ function rowPillTexts(row, state2) {
2688
2533
  function rowPills(row, state2) {
2689
2534
  return rowPillTexts(row, state2).map(([cls, text]) => el("span", { class: cls }, text));
2690
2535
  }
2691
- function renderActions(item, state2, ui2, handlers2) {
2536
+ function renderActions(item, ui2, handlers2) {
2692
2537
  const buttons = [];
2693
2538
  const act = (id, action) => () => handlers2.onAction(id, action);
2694
- const running = state2.session?.active?.id === item.id;
2695
2539
  if (item.status === "open") {
2696
2540
  if (item.kind === "task") {
2697
- buttons.push(
2698
- el(
2699
- "button",
2700
- {
2701
- type: "button",
2702
- class: running ? "button button--primary" : "button",
2703
- title: running ? "Stop the focus session" : "Start a focus session on this",
2704
- onClick: () => running ? handlers2.stopSession() : handlers2.startSession(item.id)
2705
- },
2706
- running ? "Stop" : "Focus"
2707
- )
2708
- );
2709
2541
  buttons.push(el("button", { type: "button", class: "button", onClick: act(item.id, "done") }, "Done"));
2710
2542
  buttons.push(
2711
2543
  el(
@@ -2905,8 +2737,10 @@ function renderBoard(state2, ui2, handlers2) {
2905
2737
  const open = board.rows.filter((row) => row.status === "open");
2906
2738
  for (const court of COURT_ORDER) {
2907
2739
  const rows = open.filter((row) => row.court === court);
2908
- if (rows.length === 0) continue;
2909
- parts.push(section(COURT_TITLE[court], rows, state2, ui2, handlers2, null, renderPullRow));
2740
+ const nudged = court === "reviewers" ? rows.filter((row) => row.nudgedAt) : [];
2741
+ const waiting = rows.filter((row) => !row.nudgedAt);
2742
+ if (waiting.length > 0) parts.push(section(COURT_TITLE[court], waiting, state2, ui2, handlers2, null, renderPullRow));
2743
+ if (nudged.length > 0) parts.push(section("Nudged", nudged, state2, ui2, handlers2, null, renderPullRow));
2910
2744
  }
2911
2745
  if (open.length === 0 && !board.reason) {
2912
2746
  parts.push(
@@ -2963,6 +2797,7 @@ function PullRow({ item: row, state: state2, ui: ui2, handlers: handlers2 }) {
2963
2797
  "data-id": row.id,
2964
2798
  "data-status": row.status,
2965
2799
  "data-court": row.court,
2800
+ "data-nudged": String(Boolean(row.nudgedAt)),
2966
2801
  "data-selected": String(selected),
2967
2802
  "data-pending": String(ui2.pending.value.has(row.id)),
2968
2803
  "data-detail": String(ui2.detailFor.value === row.id),
@@ -3010,6 +2845,9 @@ function renderPullMeta(row, state2, now) {
3010
2845
  if (row.mergeable === "CONFLICTING") pill("pill pill--overdue", "conflicts");
3011
2846
  if (row.autoMerge) pill("pill pill--good", "auto-merge on");
3012
2847
  if (row.nudge) pill("pill pill--age", "time to ask");
2848
+ if (row.nudgedAt && row.nudgedUntil) {
2849
+ pill("pill", `nudged ${relativeTime(row.nudgedAt, now)}, back ${relativeDay(row.nudgedUntil, now)} ${formatTime(row.nudgedUntil)}`);
2850
+ }
3013
2851
  if (row.court === "blocked") {
3014
2852
  pill("pill", `blocked ${waitedFor(row.since, now)}`);
3015
2853
  } else if (row.court === "gate" || row.court === "checks") {
@@ -3114,7 +2952,7 @@ function renderPullActions(row, ui2, handlers2) {
3114
2952
  )
3115
2953
  );
3116
2954
  if (row.status === "open") {
3117
- if (row.court === "reviewers") {
2955
+ if (row.court === "reviewers" && !row.nudgedAt) {
3118
2956
  buttons.push(
3119
2957
  el(
3120
2958
  "button",
@@ -3151,7 +2989,7 @@ var TICKET_COURT_TITLE = {
3151
2989
  };
3152
2990
  var TICKET_COURT_ORDER = ["settled", "started", "idle"];
3153
2991
  var TICKET_COURT_HINT = {
3154
- settled: "Every pull request Jira has for these is closed. Move them on, or say what is still to come \u2014 a pull request nobody has written yet is invisible from here.",
2992
+ settled: "Every pull request linked to these is closed. Move them on, or say what is still to come \u2014 a pull request nobody has written yet is invisible from here.",
3155
2993
  started: "A pull request is open, so the work has begun. The status still says it has not.",
3156
2994
  idle: "Nothing in Jira links any code to these. Fine for work that is not code; worth a look otherwise."
3157
2995
  };
@@ -3362,7 +3200,8 @@ function TicketRowView({ item: row, state: state2, ui: ui2, handlers: handlers2
3362
3200
  { class: "item__body" },
3363
3201
  renderTicketStatus(row, state2, ui2, handlers2),
3364
3202
  el("p", { class: "item__title" }, title),
3365
- renderTicketMeta(row, state2, handlers2)
3203
+ renderTicketMeta(row, state2, handlers2),
3204
+ renderGitHubEvidence(row, now)
3366
3205
  ),
3367
3206
  renderTicketActions(row, ui2, handlers2),
3368
3207
  ui2.menuFor.value === row.id ? renderSnoozeMenu(row, now, handlers2, PARK_MENU) : null
@@ -3436,6 +3275,25 @@ function renderTicketMeta(row, state2, handlers2) {
3436
3275
  pills.push(...rowPills(row, state2));
3437
3276
  return pills.length > 0 ? el("div", { class: "item__meta item__meta--extra" }, pills) : null;
3438
3277
  }
3278
+ var EVIDENCE_SHOWN = 3;
3279
+ function renderGitHubEvidence(row, now) {
3280
+ const pulls = row.pulls ?? [];
3281
+ if (!row.fromGitHub || pulls.length === 0) return null;
3282
+ const parts = ["GitHub, ahead of Jira: "];
3283
+ pulls.slice(0, EVIDENCE_SHOWN).forEach((pull, i4) => {
3284
+ if (i4 > 0) parts.push(" \xB7 ");
3285
+ parts.push(
3286
+ el("a", { href: pull.url, target: "_blank", rel: "noopener noreferrer", title: pull.title }, `${pull.repo}#${pull.number}`),
3287
+ ` ${describePull(pull, now)}`
3288
+ );
3289
+ });
3290
+ if (pulls.length > EVIDENCE_SHOWN) parts.push(` \xB7 and ${pulls.length - EVIDENCE_SHOWN} more`);
3291
+ return el("p", { class: "item__reason item__reason--waiting" }, parts);
3292
+ }
3293
+ function describePull(pull, now) {
3294
+ if (pull.state === "open" || pull.state === "draft") return pull.state;
3295
+ return pull.closedAt ? `${pull.state} ${relativeTime(pull.closedAt, now)}` : pull.state;
3296
+ }
3439
3297
  function outOfSyncFlag(row, state2, handlers2) {
3440
3298
  const hit = state2.tickets?.rows?.find((candidate) => candidate.id === row.id && candidate.status === "open");
3441
3299
  if (!hit) return null;
@@ -3746,8 +3604,6 @@ var SHORTCUTS = [
3746
3604
  [["Esc"], "Close the menu, then the panel"],
3747
3605
  [["o"], "Open the source link"],
3748
3606
  [["u"], "Undo the last action"],
3749
- [["f"], "Focus mode \u2014 hide all but the top item"],
3750
- [["p"], "Start a focus session on the selected item"],
3751
3607
  [["r"], "Refresh whichever board you are on"],
3752
3608
  [["w"], "Jira tickets: switch between out of sync and working on"],
3753
3609
  [[","], "Settings: your objective, where the agent looks, and the rest"],
@@ -4843,14 +4699,8 @@ function boardRow(name, health, text, action) {
4843
4699
 
4844
4700
  // client/today.ts
4845
4701
  function TodayView({ state: state2, ui: ui2, handlers: handlers2 }) {
4846
- const focusBar = el(
4847
- "p",
4848
- { class: "focus-bar", id: "focus-bar", hidden: !ui2.focusMode.value },
4849
- "Focus mode \u2014 everything but the top item is hidden. ",
4850
- el("button", { type: "button", class: "button", id: "focus-exit", onClick: () => handlers2.setFocusMode(false) }, "Show everything")
4851
- );
4852
- if (state2.setup?.needed) return [focusBar, k(SetupCard, { state: state2, ui: ui2, handlers: handlers2 })];
4853
- return [focusBar, renderObjective(state2, handlers2), renderStats(state2), renderHeadline(state2), renderSections(state2, ui2, handlers2)];
4702
+ if (state2.setup?.needed) return [k(SetupCard, { state: state2, ui: ui2, handlers: handlers2 })];
4703
+ return [renderObjective(state2, handlers2), renderStats(state2), renderHeadline(state2), renderSections(state2, ui2, handlers2)];
4854
4704
  }
4855
4705
  function renderObjective(state2, handlers2 = null) {
4856
4706
  const focus = state2.focus;
@@ -4973,7 +4823,6 @@ function App({ ui: ui2, handlers: handlers2 }) {
4973
4823
  el("a", { class: "skip-link", href: "#main" }, "Skip to content"),
4974
4824
  k(Header, { state: current, ui: ui2, handlers: handlers2 }),
4975
4825
  k(Tabs, { state: current, ui: ui2, handlers: handlers2 }),
4976
- current ? k(Timer, { state: current, ui: ui2, handlers: handlers2 }) : el("div", { class: "timer", id: "timer", hidden: true }),
4977
4826
  el("div", { id: "banners", class: "banners" }, current ? renderBanners(current, ui2, handlers2) : null),
4978
4827
  el(
4979
4828
  "main",
@@ -5091,11 +4940,6 @@ function installKeyboard(handlers2) {
5091
4940
  event.preventDefault();
5092
4941
  moveSelection(-1);
5093
4942
  return;
5094
- case "f":
5095
- if (ui.view.value !== "today") return;
5096
- event.preventDefault();
5097
- handlers2.setFocusMode(!ui.focusMode.value);
5098
- return;
5099
4943
  case "1":
5100
4944
  event.preventDefault();
5101
4945
  handlers2.setView("today");
@@ -5140,7 +4984,6 @@ function installKeyboard(handlers2) {
5140
4984
  if (!item) return;
5141
4985
  const boardRow2 = ui.view.value !== "today";
5142
4986
  const status = "status" in item ? item.status : null;
5143
- const kind = "kind" in item ? item.kind : null;
5144
4987
  switch (event.key) {
5145
4988
  case "e":
5146
4989
  event.preventDefault();
@@ -5163,12 +5006,6 @@ function installKeyboard(handlers2) {
5163
5006
  event.preventDefault();
5164
5007
  handlers2.openDetail(item.id, "assistant");
5165
5008
  break;
5166
- case "p":
5167
- event.preventDefault();
5168
- if (boardRow2) break;
5169
- if (state.value?.session.active?.id === item.id) handlers2.stopSession();
5170
- else if (status === "open" && kind === "task") handlers2.startSession(item.id);
5171
- break;
5172
5009
  case "o":
5173
5010
  if (item.url) {
5174
5011
  event.preventDefault();
@@ -5188,15 +5025,9 @@ R(k(App, { ui, handlers }), root);
5188
5025
  j3(() => {
5189
5026
  document.body.dataset.view = ui.view.value;
5190
5027
  });
5191
- j3(() => {
5192
- document.body.dataset.focusMode = String(ui.focusMode.value);
5193
- });
5194
5028
  j3(() => {
5195
5029
  document.body.dataset.offline = String(ui.connectionError.value !== null);
5196
5030
  });
5197
- j3(() => {
5198
- document.body.dataset.sessionActive = String(state.value?.session.active != null);
5199
- });
5200
5031
  j3(() => {
5201
5032
  const current = state.value;
5202
5033
  const open = current !== null && (detailRun(current, ui) !== null || detailRow(current, ui) !== null);
@@ -5223,7 +5054,6 @@ j3(() => {
5223
5054
  });
5224
5055
  installKeyboard(handlers);
5225
5056
  await refresh();
5226
- startLocalTick();
5227
5057
  var zone = watchZone({
5228
5058
  browserZone,
5229
5059
  dashboardZone: () => state.value?.timeZone,