daily-focus 0.1.0 → 0.1.1-dev.11

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/SETUP.md CHANGED
@@ -320,7 +320,8 @@ that runs it, and the calendar helper, so there is nothing else to install, and
320
320
  calendar permission is asked for once, by an app you recognise. It needs a Mac with
321
321
  Apple silicon and macOS 13.5 or newer.
322
322
 
323
- Open `Daily Focus.dmg` and drag the app to Applications, then open it from there.
323
+ Download `Daily-Focus.dmg` from the [latest release](https://github.com/watson/daily-focus/releases/latest),
324
+ open it and drag the app to Applications, then open it from there.
324
325
  It has no window and no Dock icon: look for it in the menu bar.
325
326
 
326
327
  To build it yourself from a checkout:
@@ -329,7 +330,7 @@ To build it yourself from a checkout:
329
330
  npm run app
330
331
  ```
331
332
 
332
- That leaves `Daily Focus.dmg` and the app itself in `macos/build/`. Its log is
333
+ That leaves `Daily-Focus.dmg` and the app itself in `macos/build/`. Its log is
333
334
  `~/Library/Logs/Daily Focus.log`. Everything the dashboard starts runs inside the
334
335
  app as far as macOS is concerned, so a permission prompt from the morning agent's
335
336
  CLI, for Reminders, Messages or your calendars, names Daily Focus, and Full Disk
@@ -8,7 +8,7 @@ import {
8
8
  readArchiveIndex,
9
9
  resolveItems,
10
10
  runContractChecks
11
- } from "./chunk-2PLWVW5T.js";
11
+ } from "./chunk-CQ53JYUU.js";
12
12
  import {
13
13
  BRIEF_REFRESH_GRACE_HOURS,
14
14
  describeSchedule,
@@ -551,7 +551,8 @@ async function readTodaysSessions(config, now) {
551
551
  }
552
552
 
553
553
  // src/store.ts
554
- import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile3 } from "node:fs/promises";
554
+ import { createHash } from "node:crypto";
555
+ import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile3, stat } from "node:fs/promises";
555
556
  import { setTimeout as delay } from "node:timers/promises";
556
557
 
557
558
  // src/agenda.ts
@@ -710,6 +711,16 @@ var Store = class {
710
711
  async ensureDataDir() {
711
712
  await mkdir3(this.config.dataDir, { recursive: true });
712
713
  }
714
+ /**
715
+ * Which store this is, as an opaque fingerprint: its path, and the directory's
716
+ * inode and birth time, so a store deleted and made again at the same path
717
+ * counts as another one. A tab compares it across reconnects; see the client's
718
+ * `checkStore`.
719
+ */
720
+ async fingerprint() {
721
+ const info = await stat(this.config.dataDir);
722
+ return createHash("sha256").update(`${this.config.dataDir}\0${info.ino}\0${info.birthtimeMs}`).digest("hex").slice(0, 16);
723
+ }
713
724
  /**
714
725
  * Read and parse items.json.
715
726
  *
package/dist/cli.js CHANGED
@@ -78,7 +78,7 @@ async function demo(open) {
78
78
  });
79
79
  }
80
80
  async function start(open, onStarted) {
81
- const { runServer } = await import("./server-B4FWZHS2.js");
81
+ const { runServer } = await import("./server-O5GF3TRI.js");
82
82
  await runServer({
83
83
  onStarted: (url) => {
84
84
  onStarted?.(url);
@@ -184,10 +184,10 @@ async function main(argv) {
184
184
  await import("./init-ZJCI2723.js");
185
185
  return;
186
186
  case "audit":
187
- await import("./audit-4AUZVELT.js");
187
+ await import("./audit-TPBQFSL3.js");
188
188
  return;
189
189
  case "seed":
190
- await import("./seed-7DIDKXT7.js");
190
+ await import("./seed-S5ZU6EIO.js");
191
191
  return;
192
192
  case "build-calendar":
193
193
  return buildCalendar();
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-NDWZUZJE.js";
4
4
  import {
5
5
  Store
6
- } from "./chunk-2PLWVW5T.js";
6
+ } from "./chunk-CQ53JYUU.js";
7
7
  import {
8
8
  loadConfig
9
9
  } from "./chunk-G4K24I4M.js";
@@ -16,7 +16,7 @@ import {
16
16
  stopSession,
17
17
  tempPathFor,
18
18
  writeJsonAtomic
19
- } from "./chunk-2PLWVW5T.js";
19
+ } from "./chunk-CQ53JYUU.js";
20
20
  import {
21
21
  CLI_NAMES,
22
22
  LAUNCH_SETTINGS,
@@ -3443,7 +3443,7 @@ var SETTINGS = [
3443
3443
  key: "DAILY_FOCUS_AGENT_MODEL",
3444
3444
  group: "agent",
3445
3445
  label: "Model",
3446
- help: "Passed to the CLI as it is, such as opus. Empty uses the CLI's own default.",
3446
+ help: "Passed to the CLI as it is, such as opus for Claude Code. Empty uses the CLI's own default.",
3447
3447
  kind: "text",
3448
3448
  fallback: always("The CLI's own")
3449
3449
  },
@@ -4132,6 +4132,7 @@ async function startServer(env, options = {}) {
4132
4132
  for (const warning of configWarnings) console.warn(`[daily-focus] ${warning}`);
4133
4133
  const store = new Store(config);
4134
4134
  await store.ensureDataDir();
4135
+ const storeId = await store.fingerprint();
4135
4136
  for (const outcome of await linkStore(config)) {
4136
4137
  if (outcome.result === "linked") console.log(`[daily-focus] linked ${describeLink(config, outcome)}`);
4137
4138
  if (outcome.result === "failed") console.warn(`[daily-focus] could not link ${outcome.name}: ${outcome.detail}`);
@@ -4191,7 +4192,8 @@ async function startServer(env, options = {}) {
4191
4192
  setupWarnings: [...configWarnings, ...linkWarning ? [linkWarning] : []],
4192
4193
  restart,
4193
4194
  setup,
4194
- assetVersion
4195
+ assetVersion,
4196
+ storeId
4195
4197
  };
4196
4198
  }
4197
4199
  async function broadcast(prebuilt) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daily-focus",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-dev.11",
4
4
  "description": "An interactive dashboard of what you should focus on today at work",
5
5
  "keywords": [
6
6
  "dashboard",
package/public/app.js CHANGED
@@ -1495,11 +1495,13 @@ function adoptState(next) {
1495
1495
  ui.connectionError.value = null;
1496
1496
  });
1497
1497
  startLocalTick();
1498
+ checkStore(next.storeId);
1498
1499
  checkAssetVersion(next.assetVersion);
1499
1500
  }
1500
1501
  async function refresh() {
1501
1502
  try {
1502
1503
  const next = await fetchState();
1504
+ checkStore(next.storeId);
1503
1505
  checkAssetVersion(next.assetVersion);
1504
1506
  n2(() => {
1505
1507
  state.value = next;
@@ -1524,6 +1526,12 @@ function checkAssetVersion(next) {
1524
1526
  }
1525
1527
  location.reload();
1526
1528
  }
1529
+ var loadedStore = null;
1530
+ function checkStore(next) {
1531
+ if (!next) return;
1532
+ loadedStore ??= next;
1533
+ if (next !== loadedStore) location.reload();
1534
+ }
1527
1535
  var PAST_TENSE = {
1528
1536
  done: "Marked done",
1529
1537
  dismiss: "Dismissed",
@@ -1954,9 +1962,9 @@ var handlers = {
1954
1962
  ui.menuFor.value = null;
1955
1963
  }),
1956
1964
  moveTicket: (key, status, from) => void moveTicket(key, status, from),
1957
- openDetail: (id, field2 = null) => n2(() => {
1965
+ openDetail: (id, field = null) => n2(() => {
1958
1966
  setDetailFor(ui, id);
1959
- ui.focusField.value = field2;
1967
+ ui.focusField.value = field;
1960
1968
  closeMenus();
1961
1969
  }),
1962
1970
  closeDetail: () => setDetailFor(ui, null),
@@ -2035,7 +2043,7 @@ var handlers = {
2035
2043
  ui.settingsDraft.value = /* @__PURE__ */ new Map();
2036
2044
  ui.settingsError.value = null;
2037
2045
  }),
2038
- saveSettings: () => void sendSettings(Object.fromEntries(ui.settingsDraft.value), true),
2046
+ saveSettings: (keys) => void sendSettings(Object.fromEntries([...ui.settingsDraft.value].filter(([key]) => !keys || keys.includes(key))), true),
2039
2047
  chooseSettings: (values) => void sendSettings(values, false),
2040
2048
  loadText: (name) => void loadText(name),
2041
2049
  editText: (name, text) => patchText(name, { draft: text }),
@@ -2645,6 +2653,7 @@ function renderActions(item, state2, ui2, handlers2) {
2645
2653
  {
2646
2654
  type: "button",
2647
2655
  class: "button",
2656
+ "aria-haspopup": "menu",
2648
2657
  "aria-expanded": String(ui2.menuFor.value === item.id),
2649
2658
  onClick: () => handlers2.toggleMenu(item.id)
2650
2659
  },
@@ -2668,9 +2677,9 @@ function renderSnoozeMenu(item, now, handlers2, { indefinite = true, longRange =
2668
2677
  presets.push(["Next month", addMonths(now, 1), true], ["Next quarter", addMonths(now, 3), true]);
2669
2678
  }
2670
2679
  let dateInput = null;
2671
- return el(
2672
- "div",
2673
- { class: "menu", role: "menu" },
2680
+ return k(
2681
+ Menu,
2682
+ { class: "menu" },
2674
2683
  presets.map(
2675
2684
  ([label, until, dated]) => el(
2676
2685
  "button",
@@ -2719,6 +2728,14 @@ function renderSnoozeMenu(item, now, handlers2, { indefinite = true, longRange =
2719
2728
  )
2720
2729
  );
2721
2730
  }
2731
+ function Menu({ class: className, children }) {
2732
+ const ref = A2(null);
2733
+ _2(() => {
2734
+ const node = ref.current;
2735
+ if (node?.showPopover && !node.matches(":popover-open")) node.showPopover();
2736
+ });
2737
+ return el("div", { class: className, role: "menu", popover: "manual", ref }, children);
2738
+ }
2722
2739
  function addDays(from, days) {
2723
2740
  return localDateKey(new Date(from.getFullYear(), from.getMonth(), from.getDate() + days));
2724
2741
  }
@@ -3061,6 +3078,7 @@ function renderPullActions(row, ui2, handlers2) {
3061
3078
  type: "button",
3062
3079
  class: "button",
3063
3080
  title: "Park this until a date",
3081
+ "aria-haspopup": "menu",
3064
3082
  "aria-expanded": String(ui2.menuFor.value === row.id),
3065
3083
  onClick: () => handlers2.toggleMenu(row.id)
3066
3084
  },
@@ -3312,6 +3330,7 @@ function renderTicketStatus(row, state2, ui2, handlers2) {
3312
3330
  type: "button",
3313
3331
  class: "pill pill--status pill--button",
3314
3332
  title: `Move ${row.key} to another status`,
3333
+ "aria-haspopup": "menu",
3315
3334
  "aria-expanded": String(ui2.statusFor.value === row.id),
3316
3335
  onClick: () => handlers2.toggleStatus(row.id)
3317
3336
  },
@@ -3328,9 +3347,9 @@ function ticketStatusOptions(row, state2) {
3328
3347
  }
3329
3348
  function renderStatusMenu(row, offered, ui2, handlers2) {
3330
3349
  const busy = ui2.pending.value.has(row.id);
3331
- return el(
3332
- "div",
3333
- { class: "menu menu--status", role: "menu" },
3350
+ return k(
3351
+ Menu,
3352
+ { class: "menu menu--status" },
3334
3353
  offered.map(
3335
3354
  (status) => el(
3336
3355
  "button",
@@ -3381,6 +3400,7 @@ function renderTicketActions(row, ui2, handlers2) {
3381
3400
  type: "button",
3382
3401
  class: "button",
3383
3402
  title: "Park this until a date",
3403
+ "aria-haspopup": "menu",
3384
3404
  "aria-expanded": String(ui2.menuFor.value === row.id),
3385
3405
  onClick: () => handlers2.toggleMenu(row.id)
3386
3406
  },
@@ -4203,13 +4223,13 @@ function SettingsView({ state: state2, ui: ui2, handlers: handlers2 }) {
4203
4223
  "section",
4204
4224
  { class: "settings-card", id: `settings-${group.id}`, key: group.id, "aria-labelledby": `settings-${group.id}-title` },
4205
4225
  el("h3", { class: "settings-card__title", id: `settings-${group.id}-title` }, group.title),
4206
- plain.map((setting) => field(setting, draft, handlers2)),
4226
+ plain.map((setting) => settingField(setting, draft, handlers2)),
4207
4227
  group.id === "calendar" ? calendarPicker(state2, ui2, handlers2, page.settings) : null,
4208
4228
  advanced.length > 0 ? el(
4209
4229
  "details",
4210
4230
  { class: "settings-card__more" },
4211
4231
  el("summary", null, "More"),
4212
- advanced.map((setting) => field(setting, draft, handlers2))
4232
+ advanced.map((setting) => settingField(setting, draft, handlers2))
4213
4233
  ) : null
4214
4234
  );
4215
4235
  }) : el("p", { class: "empty" }, "Loading the settings\u2026"),
@@ -4348,7 +4368,7 @@ function flagValue(raw) {
4348
4368
  if (raw === "") return "";
4349
4369
  return ["off", "false", "0", "no"].includes(raw.trim().toLowerCase()) ? "off" : "on";
4350
4370
  }
4351
- function field(setting, draft, handlers2) {
4371
+ function settingField(setting, draft, handlers2) {
4352
4372
  const id = `setting-${setting.key}`;
4353
4373
  const locked = setting.source === "environment";
4354
4374
  const value = shownValue(setting, draft);
@@ -4475,7 +4495,7 @@ function SetupCard({ state: state2, ui: ui2, handlers: handlers2 }) {
4475
4495
  el(
4476
4496
  "p",
4477
4497
  { class: "setup__lede" },
4478
- "Each morning an agent reads your mail, calendar, pull requests and the rest, and writes a short brief of what needs you today. A few things to tell it first. Everything here can be changed later in ",
4498
+ "Each morning an agent reads your mail, calendar, pull requests and the rest, and writes a short brief of what needs to get done today. The steps below get it ready for its first run. Everything here can be changed later in ",
4479
4499
  el("button", { type: "button", class: "link-button", onClick: () => handlers2.openSettings() }, "Settings"),
4480
4500
  "."
4481
4501
  ),
@@ -4509,11 +4529,14 @@ function SetupCard({ state: state2, ui: ui2, handlers: handlers2 }) {
4509
4529
  step(
4510
4530
  agent.cli !== null,
4511
4531
  "Which agent writes the brief?",
4512
- agent.cli ? el(
4513
- "p",
4514
- null,
4515
- `${CLI_LABEL[agent.cli]}. ` + (agent.schedule ? `After the first brief, it runs by itself each scheduled morning at ${agent.schedule.at}. ` : "It runs when you press refresh. ") + "It has to be logged in; the first run says if it is not."
4516
- ) : agentChoice(state2, handlers2)
4532
+ agent.cli ? [
4533
+ el(
4534
+ "p",
4535
+ null,
4536
+ `${CLI_LABEL[agent.cli]}. ` + (agent.schedule ? `After the first brief, it runs by itself each scheduled morning at ${agent.schedule.at}. ` : "It runs when you press refresh. ") + "It has to be logged in; the first run says if it is not."
4537
+ ),
4538
+ agentAdvanced(agent.cli, ui2, handlers2)
4539
+ ] : agentChoice(state2, handlers2)
4517
4540
  ),
4518
4541
  step(setup.sources.exists && setup.sources.placeholders === 0, "Who are you, and where should it look?", sourcesStep(state2, ui2, handlers2)),
4519
4542
  step(null, "What else should the dashboard show?", boards(state2, handlers2)),
@@ -4586,6 +4609,43 @@ function agentChoice(state2, handlers2) {
4586
4609
  )
4587
4610
  ];
4588
4611
  }
4612
+ var AGENT_ADVANCED = "setup:agent-advanced";
4613
+ function agentAdvanced(cli, ui2, handlers2) {
4614
+ const keys = ["DAILY_FOCUS_AGENT_MODEL", "DAILY_FOCUS_AGENT_EFFORT", ...cli === "claude" ? ["DAILY_FOCUS_AGENT_TOOLS"] : []];
4615
+ const page = ui2.settings.value;
4616
+ const draft = ui2.settingsDraft.value;
4617
+ const changed = keys.some((key) => draft.has(key));
4618
+ const error = changed ? ui2.settingsError.value : null;
4619
+ return el(
4620
+ "details",
4621
+ {
4622
+ class: "setup__advanced",
4623
+ open: ui2.openDrawers.value.has(AGENT_ADVANCED),
4624
+ onToggle: (event) => {
4625
+ const open = event.currentTarget.open;
4626
+ if (open && !ui2.settings.peek()) handlers2.loadSettings();
4627
+ handlers2.toggleDrawer(AGENT_ADVANCED, open);
4628
+ }
4629
+ },
4630
+ el("summary", null, cli === "claude" ? "Advanced: model, effort and tools" : "Advanced: model and effort"),
4631
+ page ? [
4632
+ ...keys.flatMap((key) => {
4633
+ const setting = page.settings.find((entry) => entry.key === key);
4634
+ return setting ? [settingField(setting, draft, handlers2)] : [];
4635
+ }),
4636
+ error ? el("p", { class: "setup__note setup__note--todo", role: "alert" }, error) : null,
4637
+ el(
4638
+ "div",
4639
+ { class: "setup__advanced-actions" },
4640
+ el(
4641
+ "button",
4642
+ { type: "button", class: "button", disabled: !changed || ui2.settingsSaving.value, onClick: () => handlers2.saveSettings(keys) },
4643
+ ui2.settingsSaving.value ? "Saving\u2026" : "Save"
4644
+ )
4645
+ )
4646
+ ] : el("p", null, "Loading the settings\u2026")
4647
+ );
4648
+ }
4589
4649
  var SOURCES_EDITOR = "setup:sources";
4590
4650
  function sourcesStep(state2, ui2, handlers2) {
4591
4651
  const sources = state2.setup.sources;
package/public/style.css CHANGED
@@ -1152,14 +1152,29 @@ body[data-session-active='true'] #today .item:not([data-running='true']):focus-w
1152
1152
 
1153
1153
  /* snooze menu */
1154
1154
 
1155
+ /*
1156
+ * A popover in the top layer (`Menu` in client/items.ts says why), hung from the
1157
+ * button that opened it. Only one menu is ever open, so only one button is
1158
+ * expanded and the one anchor name is never ambiguous. `inset` and `margin`
1159
+ * undo the popover's own centring on the viewport.
1160
+ */
1161
+ [aria-haspopup='menu'][aria-expanded='true'] {
1162
+ anchor-name: --menu;
1163
+ }
1164
+
1155
1165
  .menu {
1156
- position: absolute;
1157
- top: 34px;
1158
- right: 10px;
1159
- z-index: 5;
1166
+ position: fixed;
1167
+ position-anchor: --menu;
1168
+ inset: auto;
1169
+ top: anchor(bottom);
1170
+ right: anchor(right);
1171
+ margin: 4px 0;
1172
+ /* Above the button instead, when the window has no room left below it. */
1173
+ position-try-fallbacks: flip-block;
1160
1174
  /* Wide enough that the date field below still shows its own placeholder. */
1161
1175
  min-width: 204px;
1162
1176
  padding: 5px;
1177
+ color: var(--ink-2);
1163
1178
  background: var(--surface);
1164
1179
  border: 1px solid var(--border);
1165
1180
  border-radius: var(--radius-sm);
@@ -1636,10 +1651,11 @@ body[data-view='settings'] .layout {
1636
1651
  * line up with the other tabs' however wide the display is. The 460px floor is
1637
1652
  * what drops it to one column on a narrow window rather than squeezing two.
1638
1653
  *
1639
- * The abspos worry that picked the grid does not apply: `.item` is
1640
- * `position: relative`, so `.item__actions` and the snooze `.menu` resolve
1641
- * against the card and travel with it into its column. Only a child resolving
1642
- * against the multicol container itself would land beside the wrong row.
1654
+ * An absolutely positioned child is still fragmented by the columns, and counts
1655
+ * toward balancing them. `.item__actions` stays inside its card, so it travels
1656
+ * with the card into its column and nothing notices. Anything that hangs below a
1657
+ * card must not be in here at all: the row menus once were, and were split
1658
+ * across both columns and pushed the next section down. They are popovers now.
1643
1659
  */
1644
1660
  #tickets .list {
1645
1661
  display: block;
@@ -1836,14 +1852,13 @@ body[data-view='settings'] .layout {
1836
1852
  }
1837
1853
 
1838
1854
  /*
1839
- * Anchored to the gutter rather than the card's right edge, unlike the park
1840
- * menu: it belongs to the pill that opened it, and the two menus must not be
1841
- * able to appear in the same place.
1855
+ * Hung from the pill's left edge, where the park menu hangs from its button's
1856
+ * right: the pill sits in the card's left gutter, so each menu opens toward the
1857
+ * middle of its own card.
1842
1858
  */
1843
1859
  .menu--status {
1844
- top: 26px;
1845
1860
  right: auto;
1846
- left: 0;
1861
+ left: anchor(left);
1847
1862
  min-width: 9rem;
1848
1863
  }
1849
1864
 
@@ -2317,6 +2332,19 @@ body[data-flyout='open'] .layout__side {
2317
2332
  .setup__editor {
2318
2333
  margin-top: 12px;
2319
2334
  }
2335
+ /* The settings page's own fold and fields, inside a step. */
2336
+ .setup__body .setup__advanced p {
2337
+ margin: 0;
2338
+ font-size: 12px;
2339
+ line-height: 1.45;
2340
+ }
2341
+ .setup__advanced .field {
2342
+ max-width: 480px;
2343
+ }
2344
+ .setup__advanced-actions {
2345
+ display: flex;
2346
+ justify-content: flex-start;
2347
+ }
2320
2348
 
2321
2349
  /* ---------- settings ---------- */
2322
2350
 
@@ -2365,16 +2393,19 @@ body[data-flyout='open'] .layout__side {
2365
2393
  color: var(--serious);
2366
2394
  font-size: 13px;
2367
2395
  }
2368
- .settings-card__more {
2396
+ .settings-card__more,
2397
+ .setup__advanced {
2369
2398
  margin-top: 6px;
2370
2399
  }
2371
- .settings-card__more > summary {
2400
+ .settings-card__more > summary,
2401
+ .setup__advanced > summary {
2372
2402
  cursor: pointer;
2373
2403
  color: var(--ink-2);
2374
2404
  font-size: 13px;
2375
2405
  padding: 4px 0;
2376
2406
  }
2377
- .settings-card__more[open] > summary {
2407
+ .settings-card__more[open] > summary,
2408
+ .setup__advanced[open] > summary {
2378
2409
  margin-bottom: 10px;
2379
2410
  }
2380
2411