daily-focus 1.2.0-dev.11 → 1.2.0-dev.13

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/README.md CHANGED
@@ -163,8 +163,13 @@ in `DAILY_FOCUS_AGENT_AT`, and from the refresh icon beside the brief's age when
163
163
  morning's brief is out of date by lunch. Each run keeps what you have marked done,
164
164
  snoozed or noted.
165
165
 
166
+ The agent reads your sources in parallel, handing each to a subagent of its own.
167
+ While it runs, its panel shows what it is doing: what it says, each command it runs
168
+ and each thread or page it reads, and each subagent under the step that started it,
169
+ open while it works.
170
+
166
171
  Every run is recorded. Click "updated … ago" to open the latest: its report says what
167
- it wrote and what it could not reach, its working commentary is folded away beneath,
172
+ it wrote and what it could not reach, how it got there is folded away beneath,
168
173
  and you can ask it why it did what it did, or to look again, in the same session. Earlier runs are folded away below. See
169
174
  [SETUP.md](SETUP.md#connect-your-morning-agent).
170
175
 
package/SETUP.md CHANGED
@@ -414,10 +414,17 @@ in the archive. While it runs, a banner says so and offers Stop; click the banne
414
414
  to watch the run. When it finishes the banner goes, since the brief it wrote is the
415
415
  page. A run that fails stays as a warning until you dismiss it.
416
416
 
417
+ The agent reads your sources in parallel, each handed to a subagent of its own, and
418
+ the panel shows what it is doing as it goes: what it says, each command it runs and
419
+ each thing it reads, and each subagent under the step that started it, open while
420
+ it works. Claude Code reports its subagents itself. Codex doesn't, so for a Codex
421
+ run the dashboard reads them from Codex's own session files, under `~/.codex/sessions`
422
+ or `$CODEX_HOME`. It only ever reads them.
423
+
417
424
  Click "updated … ago" to open the latest run in the panel. It holds the agent's
418
425
  report — what it wrote, what it dropped as handled, and any source it could not
419
- reach — with everything it said on the way there folded away beneath, and a field
420
- to ask it about the run. The question goes to the agent in the
426
+ reach — with everything it said and did on the way there folded away beneath, and a
427
+ field to ask it about the run. The question goes to the agent in the
421
428
  same session, so it answers from what it actually did: why it dropped an item, what
422
429
  it could not find, whether it can find it now. A follow-up is given nothing to
423
430
  write; if something in the brief should change, it says what, and you run it again.
@@ -613,6 +620,7 @@ matters when `DAILY_FOCUS_FREE_WINDOWS` is off.
613
620
  | `DAILY_FOCUS_AGENT_BIN` | *the CLI's name* | Path to that CLI, for when the server's PATH lacks it. `~` is expanded |
614
621
  | `DAILY_FOCUS_AGENT_MODEL` | *the CLI's own* | Model passed to the CLI untouched. Unset passes no flag |
615
622
  | `DAILY_FOCUS_AGENT_EFFORT` | *the CLI's own* | Effort passed to the CLI untouched. Unset passes no flag |
623
+ | `DAILY_FOCUS_AGENT_SEARCH_TRASH` | `off` | Whether the agent searches the mail account's Trash as well as the inbox and archive. The dashboard writes it to `agent-settings.json` in the store before each run, where the agent reads it |
616
624
  | `DAILY_FOCUS_AGENT_TOOLS` | `Bash(gh *),Bash(acli *),WebFetch,mcp__claude_ai_Gmail` | What Claude Code may use to reach your sources, in its permission syntax, comma-separated. Writing `items.json`, and nothing else in the store, is allowed regardless. The default allows every Gmail tool, sending included. Ignored by Codex |
617
625
 
618
626
  ## If something looks wrong
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  readSystemZone,
3
3
  startingZone
4
- } from "./chunk-GVLNDQVQ.js";
4
+ } from "./chunk-HEP46XRX.js";
5
5
  import {
6
6
  linkState
7
7
  } from "./chunk-QKT2FAB5.js";
@@ -13,7 +13,7 @@ import {
13
13
  resolveItems,
14
14
  runContractChecks,
15
15
  setsStatus
16
- } from "./chunk-CPJ6LOVC.js";
16
+ } from "./chunk-D3JAWEF7.js";
17
17
  import {
18
18
  BRIEF_REFRESH_GRACE_HOURS,
19
19
  describeSchedule,
@@ -23,7 +23,7 @@ import {
23
23
  resolveSchedule,
24
24
  runsOn,
25
25
  workingMsBetween
26
- } from "./chunk-4DZEBUPU.js";
26
+ } from "./chunk-DPMHC72S.js";
27
27
  import {
28
28
  command
29
29
  } from "./chunk-BC2W7NND.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  canonicalId,
3
3
  foldActionLog
4
- } from "./chunk-CPJ6LOVC.js";
4
+ } from "./chunk-D3JAWEF7.js";
5
5
  import {
6
6
  laterISO,
7
7
  localDateKey,
8
8
  parseISO
9
- } from "./chunk-4DZEBUPU.js";
9
+ } from "./chunk-DPMHC72S.js";
10
10
 
11
11
  // src/codex.ts
12
12
  import { execFile } from "node:child_process";
@@ -299,6 +299,10 @@ var claudeAdapter = {
299
299
  const event = parseLine(line);
300
300
  if (!event) return null;
301
301
  const type = event.type;
302
+ if (type === "system" && event.subtype === "task_notification" && typeof event.tool_use_id === "string") {
303
+ const step = { id: event.tool_use_id, state: event.status === "completed" ? "done" : "failed", final: true };
304
+ return { steps: [step] };
305
+ }
302
306
  if (type === "system" && typeof event.session_id === "string") {
303
307
  return { sessionId: event.session_id };
304
308
  }
@@ -310,7 +314,9 @@ var claudeAdapter = {
310
314
  if (text) parsed.text = text;
311
315
  const steps = content.flatMap((block) => claudeStep(block) ?? []);
312
316
  if (steps.length) parsed.steps = steps;
313
- return text || steps.length ? parsed : null;
317
+ if (!text && !steps.length) return null;
318
+ if (typeof event.parent_tool_use_id === "string") parsed.by = event.parent_tool_use_id;
319
+ return parsed;
314
320
  }
315
321
  if (type === "result") {
316
322
  const ok = event.is_error !== true && event.subtype === "success";
@@ -369,6 +375,8 @@ var ADAPTERS = {
369
375
  codex: codexAdapter
370
376
  };
371
377
  var CLAUDE_QUIET_TOOLS = /* @__PURE__ */ new Set(["ToolSearch", "TodoWrite", "StructuredOutput"]);
378
+ var HANDOFF = "Hand part of it to a subagent";
379
+ var WAIT = "Wait for its subagents";
372
380
  function claudeStep(block) {
373
381
  if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
374
382
  return { id: block.tool_use_id, state: block.is_error === true ? "failed" : "done" };
@@ -393,7 +401,7 @@ function claudeStep(block) {
393
401
  return step("List files", said("pattern"));
394
402
  case "Task":
395
403
  case "Agent":
396
- return step("Hand part of it to a subagent", said("description"));
404
+ return { ...step(HANDOFF, said("description")), handoff: true };
397
405
  }
398
406
  const mcp = /^mcp__(.+?)__(.+)$/.exec(block.name);
399
407
  if (mcp) return step(connectorLabel(mcp[1], mcp[2]), compactArgs(input));
@@ -415,6 +423,7 @@ function codexStep(item, completed) {
415
423
  if (item.type === "mcp_tool_call" && typeof item.server === "string" && typeof item.tool === "string") {
416
424
  return { id: item.id, text: connectorLabel(item.server, item.tool), detail: compactArgs(item.arguments), state };
417
425
  }
426
+ if (item.type === "collab_tool_call" && item.tool === "wait") return { id: item.id, text: WAIT, detail: null, state };
418
427
  if (item.type === "web_search") {
419
428
  const action = item.action ?? {};
420
429
  if (action.type === "open_page") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
@@ -1010,6 +1019,12 @@ export {
1010
1019
  codexLatestModel,
1011
1020
  QUICK_ACTIONS,
1012
1021
  ADAPTERS,
1022
+ HANDOFF,
1023
+ WAIT,
1024
+ unwrapShell,
1025
+ connectorLabel,
1026
+ compactArgs,
1027
+ shorten,
1013
1028
  Assistant,
1014
1029
  autoKeys,
1015
1030
  actionOf,
@@ -14,7 +14,7 @@ import {
14
14
  startOfLocalDay,
15
15
  workingDaysSince,
16
16
  workingMsBetween
17
- } from "./chunk-4DZEBUPU.js";
17
+ } from "./chunk-DPMHC72S.js";
18
18
 
19
19
  // src/ids.ts
20
20
  function canonicalId(id) {
@@ -387,7 +387,8 @@ function envAgent(env) {
387
387
  binPath: expandHome((env.DAILY_FOCUS_AGENT_BIN ?? "").trim() || (cli ?? "codex")),
388
388
  model: envString("DAILY_FOCUS_AGENT_MODEL", "", env) || null,
389
389
  effort: envString("DAILY_FOCUS_AGENT_EFFORT", "", env) || null,
390
- tools: tools.length > 0 ? tools : DEFAULT_AGENT_TOOLS
390
+ tools: tools.length > 0 ? tools : DEFAULT_AGENT_TOOLS,
391
+ searchTrash: envFlag("DAILY_FOCUS_AGENT_SEARCH_TRASH", false, env)
391
392
  };
392
393
  }
393
394
  function envTimeOfDay(name, env) {
@@ -463,6 +464,7 @@ function loadConfig(env = loadEnv()) {
463
464
  assistantPromptSource: resolve2(import.meta.dirname, "..", "prompts", "assistant.md"),
464
465
  agent: envAgent(env),
465
466
  agentLogFile: resolve2(dataDir, "agent.jsonl"),
467
+ agentSettingsFile: resolve2(dataDir, "agent-settings.json"),
466
468
  onboardingFile: resolve2(dataDir, "onboarding.json"),
467
469
  onboardingDir: resolve2(dataDir, "onboarding"),
468
470
  onboardingPromptSource: resolve2(import.meta.dirname, "..", "prompts", "onboarding.md")
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  writeJsonAtomic
3
- } from "./chunk-CPJ6LOVC.js";
3
+ } from "./chunk-D3JAWEF7.js";
4
4
  import {
5
5
  localTimeZone
6
- } from "./chunk-4DZEBUPU.js";
6
+ } from "./chunk-DPMHC72S.js";
7
7
 
8
8
  // src/zone.ts
9
9
  import { readFile, readlink } from "node:fs/promises";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  localDateKey
3
- } from "./chunk-4DZEBUPU.js";
3
+ } from "./chunk-DPMHC72S.js";
4
4
 
5
5
  // src/sample.ts
6
6
  function timeOn(now, hour, minute = 0) {
package/dist/cli.js CHANGED
@@ -49,7 +49,7 @@ function shouldOpen(flag) {
49
49
  return flag && process.stdout.isTTY === true && !process.env.CI;
50
50
  }
51
51
  async function demo(open, profile) {
52
- const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-UMGG3VKY.js");
52
+ const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-AJWHY6KY.js");
53
53
  const chosen = demoProfile(profile);
54
54
  const store = await mkdtemp(join(tmpdir(), "daily-focus-demo-"));
55
55
  process.on("exit", () => rmSync(store, { recursive: true, force: true }));
@@ -66,7 +66,7 @@ async function demo(open, profile) {
66
66
  );
67
67
  }
68
68
  async function start(open, onStarted, options = {}) {
69
- const { runServer } = await import("./server-EDPYEZCP.js");
69
+ const { runServer } = await import("./server-VPSSCDBW.js");
70
70
  await runServer({
71
71
  ...options,
72
72
  onStarted: (url) => {
@@ -163,20 +163,20 @@ async function main(argv) {
163
163
  }
164
164
  return start(open);
165
165
  case "service": {
166
- const { main: service } = await import("./service-XPFMYIHR.js");
166
+ const { main: service } = await import("./service-RRHZ7BJN.js");
167
167
  process.exitCode = await service(values.remove ? ["--remove"] : []);
168
168
  return;
169
169
  }
170
170
  // These three are scripts that do their work as they load, and read their
171
171
  // own flags from the command line.
172
172
  case "init":
173
- await import("./init-MFPXASX3.js");
173
+ await import("./init-BZSCAS3L.js");
174
174
  return;
175
175
  case "audit":
176
- await import("./audit-F26VGPOO.js");
176
+ await import("./audit-JVDLHD3B.js");
177
177
  return;
178
178
  case "seed":
179
- await import("./seed-S32JX5MW.js");
179
+ await import("./seed-WQEXDK2L.js");
180
180
  return;
181
181
  case "build-calendar":
182
182
  return buildCalendar();
@@ -3,17 +3,17 @@ import {
3
3
  actionOf,
4
4
  autoKeys,
5
5
  judge
6
- } from "./chunk-EV44BC2F.js";
6
+ } from "./chunk-6COKG3VL.js";
7
7
  import {
8
8
  dateFromNow,
9
9
  sampleBrief,
10
10
  sampleGeneratedAt
11
- } from "./chunk-HVCCIZGA.js";
12
- import "./chunk-CPJ6LOVC.js";
11
+ } from "./chunk-TUZHZ3NP.js";
12
+ import "./chunk-D3JAWEF7.js";
13
13
  import {
14
14
  PROFILES,
15
15
  localDateKey
16
- } from "./chunk-4DZEBUPU.js";
16
+ } from "./chunk-DPMHC72S.js";
17
17
  import "./chunk-BC2W7NND.js";
18
18
 
19
19
  // src/demo.ts
@@ -619,7 +619,7 @@ function runLines(seed) {
619
619
  return lines2;
620
620
  }
621
621
  const report = seed.messages.at(-1) ?? "";
622
- lines2.push({ event: "finished", run: seed.id, sessionId: `sample-${seed.id}`, report, messages: seed.messages, at: endedAt });
622
+ lines2.push({ event: "finished", run: seed.id, sessionId: `sample-${seed.id}`, report, messages: seed.messages, steps: seed.steps ?? [], at: endedAt });
623
623
  if (seed.question) {
624
624
  const turn = `${seed.id}-q1`;
625
625
  const replied = new Date(new Date(seed.question.at).getTime() + 48e3).toISOString();
@@ -741,8 +741,75 @@ function demoAgentLog(profile, now) {
741
741
  }
742
742
  }
743
743
  ];
744
+ const newest = runs.find((run) => run.id === "run-0");
745
+ newest.steps = demoRunSteps(profile, newest.messages);
744
746
  return runs.flatMap(runLines);
745
747
  }
748
+ function demoRunSteps(profile, messages) {
749
+ const said2 = (text, by = null) => ({ kind: "message", text, detail: null, state: "done", by });
750
+ const call2 = (text, detail, by = null) => ({ kind: "tool", text, detail, state: "done", by });
751
+ const handoff = (name) => ({ ...call2("Hand part of it to a subagent", name), subagent: name });
752
+ const wait = call2("Wait for its subagents", null);
753
+ const start = [
754
+ said2(messages[0]),
755
+ call2("Run a command", "cat prompt.md focus.md sources.md items.json"),
756
+ call2("Run a command", "tail -n 400 actions.jsonl")
757
+ ];
758
+ const finish = [call2("Run a command", "mv items.json.tmp items.json"), said2(messages.at(-1))];
759
+ if (profile === "work") {
760
+ return [
761
+ ...start,
762
+ handoff("calendar"),
763
+ handoff("gmail"),
764
+ handoff("github"),
765
+ call2("Google calendar list events", '{"calendar_id":"primary","time_min":"today 00:00"}', "calendar"),
766
+ said2("5 events. The 1:1 with Alice and the design review both sit at 11:00; the lunch & learn is marked free.", "calendar"),
767
+ call2("Gmail search emails", '{"query":"newer_than:3d -category:promotions"}', "gmail"),
768
+ call2("Gmail batch read email", '{"message_ids":["19a2f\u2026","19a31\u2026","19a33\u2026","19a35\u2026"]}', "gmail"),
769
+ said2("31 threads in the window. 4 ask something of you; Bob followed up on Q4 headcount yesterday.", "gmail"),
770
+ call2("Run a command", "gh search prs --review-requested=@me --state=open --json url,title,updatedAt", "github"),
771
+ call2("Run a command", "gh pr checks 3402 -R acme/webapp", "github"),
772
+ said2("A review request from Alice on acme/webapp#3421, and a red check on #3402. The rest is already on the board.", "github"),
773
+ wait,
774
+ said2(messages[1]),
775
+ said2(messages[2]),
776
+ handoff("jira"),
777
+ handoff("slack"),
778
+ handoff("workday"),
779
+ call2("Run a command", 'acli jira workitem search --jql "assignee = currentUser() AND statusCategory != Done"', "jira"),
780
+ said2("PROJ-8842 is still open, with both of its pull requests merged.", "jira"),
781
+ call2("Slack search messages", '{"query":"to:me after:yesterday"}', "slack"),
782
+ said2("One direct question, from Chris in #webapp-dev.", "slack"),
783
+ call2("Workday list inbox tasks", null, "workday"),
784
+ said2("The compliance training is due tonight. The Berlin expense report is still a draft.", "workday"),
785
+ { ...wait },
786
+ said2(messages[3]),
787
+ ...finish
788
+ ];
789
+ }
790
+ return [
791
+ ...start,
792
+ handoff("calendar"),
793
+ handoff("eboks"),
794
+ handoff("email"),
795
+ call2("Google calendar list events", '{"calendar_id":"personal","time_min":"today 00:00"}', "calendar"),
796
+ call2("Google calendar list events", '{"calendar_id":"family","time_min":"today 00:00"}', "calendar"),
797
+ said2("6 events across Personal and Family. The insurance call at 16:30 overlaps the daycare pickup.", "calendar"),
798
+ call2("Run a command", "eboks list --since 3d --json", "eboks"),
799
+ call2("Run a command", "eboks text --json 4417911204", "eboks"),
800
+ said2("5 letters. The daycare invoice and the tax assessment ask for something; three are confirmations.", "eboks"),
801
+ call2("Gmail search emails", '{"query":"newer_than:3d -category:promotions"}', "email"),
802
+ said2("3 threads need you, the insulation quote among them, quiet for nine days.", "email"),
803
+ wait,
804
+ said2(messages[1]),
805
+ handoff("messages"),
806
+ call2("Run a command", "messages recent --since 3d --json", "messages"),
807
+ said2("Jonas asked about Saturday on Tuesday, and Priya is still waiting for the photos.", "messages"),
808
+ { ...wait },
809
+ said2(messages[2]),
810
+ ...finish
811
+ ];
812
+ }
746
813
  var call = (text, detail, state = "done") => ({ kind: "tool", text, detail, state });
747
814
  var said = (text) => ({ kind: "message", text, detail: null, state: "done" });
748
815
  function turnLines(seed) {
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-QKT2FAB5.js";
9
9
  import {
10
10
  loadConfig
11
- } from "./chunk-4DZEBUPU.js";
11
+ } from "./chunk-DPMHC72S.js";
12
12
  import {
13
13
  PACKAGED,
14
14
  command
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  sampleBrief
3
- } from "./chunk-HVCCIZGA.js";
3
+ } from "./chunk-TUZHZ3NP.js";
4
4
  import {
5
5
  Store
6
- } from "./chunk-CPJ6LOVC.js";
6
+ } from "./chunk-D3JAWEF7.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-4DZEBUPU.js";
9
+ } from "./chunk-DPMHC72S.js";
10
10
  import {
11
11
  command
12
12
  } from "./chunk-BC2W7NND.js";
@@ -3,13 +3,19 @@ import {
3
3
  Assistant,
4
4
  AutoAsk,
5
5
  CodexGate,
6
+ HANDOFF,
7
+ WAIT,
6
8
  codexLatestModel,
7
9
  codexVersionWarning,
10
+ compactArgs,
8
11
  computerUseOff,
12
+ connectorLabel,
9
13
  countBoard,
10
14
  refusalFor,
11
- resolveBoard
12
- } from "./chunk-EV44BC2F.js";
15
+ resolveBoard,
16
+ shorten,
17
+ unwrapShell
18
+ } from "./chunk-6COKG3VL.js";
13
19
  import {
14
20
  FOCUS_TEMPLATE,
15
21
  sourcesTemplate
@@ -17,7 +23,7 @@ import {
17
23
  import {
18
24
  TimeZoneKeeper,
19
25
  validZone
20
- } from "./chunk-GVLNDQVQ.js";
26
+ } from "./chunk-HEP46XRX.js";
21
27
  import {
22
28
  describeLink,
23
29
  linkStore,
@@ -30,7 +36,7 @@ import {
30
36
  foldActionLog,
31
37
  tempPathFor,
32
38
  writeJsonAtomic
33
- } from "./chunk-CPJ6LOVC.js";
39
+ } from "./chunk-D3JAWEF7.js";
34
40
  import {
35
41
  CLI_NAMES,
36
42
  LAUNCH_SETTINGS,
@@ -46,7 +52,7 @@ import {
46
52
  readDotEnv,
47
53
  resolveSchedule,
48
54
  scheduledRunDue
49
- } from "./chunk-4DZEBUPU.js";
55
+ } from "./chunk-DPMHC72S.js";
50
56
  import {
51
57
  PACKAGED,
52
58
  command,
@@ -58,7 +64,7 @@ import {
58
64
  import { createServer } from "node:http";
59
65
  import { createReadStream } from "node:fs";
60
66
  import { stat as stat4 } from "node:fs/promises";
61
- import { basename, extname, join as join6, normalize, resolve, sep } from "node:path";
67
+ import { basename, extname, join as join7, normalize, resolve, sep } from "node:path";
62
68
  import { fileURLToPath } from "node:url";
63
69
 
64
70
  // src/clis.ts
@@ -2729,11 +2735,170 @@ async function readTicketsFile(path) {
2729
2735
  import { spawn } from "node:child_process";
2730
2736
  import { randomUUID } from "node:crypto";
2731
2737
  import { appendFile, mkdir, readFile as readFile7 } from "node:fs/promises";
2732
- import { homedir as homedir2 } from "node:os";
2738
+ import { homedir as homedir3 } from "node:os";
2733
2739
  import { createInterface } from "node:readline";
2740
+
2741
+ // src/codexsessions.ts
2742
+ import { open, readdir as readdir2 } from "node:fs/promises";
2743
+ import { homedir as homedir2 } from "node:os";
2744
+ import { join as join4 } from "node:path";
2745
+ function codexHome(env = process.env) {
2746
+ return env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
2747
+ }
2748
+ var DAY_MS = 24 * 60 * 6e4;
2749
+ var CHUNK = 4 * 1024 * 1024;
2750
+ var CodexSessions = class {
2751
+ threads = /* @__PURE__ */ new Map();
2752
+ since;
2753
+ home;
2754
+ /** `threadId` is the run's, as the stream's `thread.started` gives it; `since`, when the run started. */
2755
+ constructor(threadId, since, home = codexHome()) {
2756
+ this.since = since;
2757
+ this.home = home;
2758
+ this.threads.set(threadId, { id: threadId, root: true, path: null, offset: 0, partial: Buffer.alloc(0) });
2759
+ }
2760
+ /**
2761
+ * Whatever the files have added since the last read, oldest first within each
2762
+ * thread. A subagent found on the way is read in the same pass.
2763
+ */
2764
+ async read(now = /* @__PURE__ */ new Date()) {
2765
+ const news = [];
2766
+ for (const thread of this.threads.values()) {
2767
+ thread.path ??= await this.find(thread.id, now);
2768
+ if (!thread.path) continue;
2769
+ for (const line of await this.tail(thread)) this.take(thread, line, news);
2770
+ }
2771
+ return news;
2772
+ }
2773
+ /**
2774
+ * The file a thread is written to. Codex files a session under the local date
2775
+ * it started, so the days the run spans are searched, a day either side for a
2776
+ * clock that disagrees.
2777
+ */
2778
+ async find(id, now) {
2779
+ const suffix = `-${id}.jsonl`;
2780
+ for (let at = this.since.getTime() - DAY_MS; at <= now.getTime() + DAY_MS; at += DAY_MS) {
2781
+ const day = new Date(at);
2782
+ const dir = join4(
2783
+ this.home,
2784
+ "sessions",
2785
+ String(day.getFullYear()),
2786
+ String(day.getMonth() + 1).padStart(2, "0"),
2787
+ String(day.getDate()).padStart(2, "0")
2788
+ );
2789
+ let names;
2790
+ try {
2791
+ names = await readdir2(dir);
2792
+ } catch {
2793
+ continue;
2794
+ }
2795
+ const name = names.find((candidate) => candidate.endsWith(suffix));
2796
+ if (name) return join4(dir, name);
2797
+ }
2798
+ return null;
2799
+ }
2800
+ /** The whole lines added to a thread's file since the last read. */
2801
+ async tail(thread) {
2802
+ let handle;
2803
+ try {
2804
+ handle = await open(thread.path, "r");
2805
+ } catch {
2806
+ return [];
2807
+ }
2808
+ try {
2809
+ const { size } = await handle.stat();
2810
+ if (size <= thread.offset) return [];
2811
+ const buffer = Buffer.alloc(Math.min(size - thread.offset, CHUNK));
2812
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, thread.offset);
2813
+ thread.offset += bytesRead;
2814
+ const bytes = Buffer.concat([thread.partial, buffer.subarray(0, bytesRead)]);
2815
+ const end = bytes.lastIndexOf(10);
2816
+ thread.partial = end === -1 ? bytes : bytes.subarray(end + 1);
2817
+ return end === -1 ? [] : bytes.subarray(0, end).toString("utf8").split("\n");
2818
+ } catch {
2819
+ return [];
2820
+ } finally {
2821
+ await handle.close();
2822
+ }
2823
+ }
2824
+ take(thread, line, news) {
2825
+ let record;
2826
+ try {
2827
+ record = JSON.parse(line);
2828
+ } catch {
2829
+ return;
2830
+ }
2831
+ if (record?.type !== "event_msg" || record.payload?.type !== "item_completed") return;
2832
+ const item = record.payload.item;
2833
+ if (typeof item !== "object" || item === null) return;
2834
+ const by = thread.root ? null : thread.id;
2835
+ if (item.type === "SubAgentActivity" && typeof item.agent_thread_id === "string") {
2836
+ const child = item.agent_thread_id;
2837
+ if (item.kind === "started") {
2838
+ if (!this.threads.has(child)) {
2839
+ this.threads.set(child, { id: child, root: false, path: null, offset: 0, partial: Buffer.alloc(0) });
2840
+ }
2841
+ news.push({ by, step: { id: child, text: HANDOFF, detail: subagentName(item.agent_path), state: "running", handoff: true } });
2842
+ } else if (item.kind === "completed") {
2843
+ news.push({ by, step: { id: child, state: "done", final: true } });
2844
+ } else if (typeof item.kind === "string" && /fail|error|abort|interrupt|cancel|kill/i.test(item.kind)) {
2845
+ news.push({ by, step: { id: child, state: "failed", final: true } });
2846
+ }
2847
+ return;
2848
+ }
2849
+ if (thread.root) return;
2850
+ if (item.type === "AgentMessage") {
2851
+ const text = Array.isArray(item.content) ? item.content.map((part) => typeof part?.text === "string" ? part.text : "").join("").trim() : "";
2852
+ if (text) news.push({ by: thread.id, message: text });
2853
+ return;
2854
+ }
2855
+ const step = rolloutStep(item);
2856
+ if (step) news.push({ by: thread.id, step: { ...step, id: `${thread.id}:${step.id}` } });
2857
+ }
2858
+ };
2859
+ function subagentName(path) {
2860
+ if (typeof path !== "string") return null;
2861
+ const name = path.split("/").filter(Boolean).at(-1);
2862
+ return name ? name.replace(/[_-]+/g, " ") : null;
2863
+ }
2864
+ function rolloutStep(item) {
2865
+ if (typeof item.id !== "string") return null;
2866
+ const failed = item.status === "failed" || item.error !== void 0 && item.error !== null;
2867
+ const state = failed ? "failed" : "done";
2868
+ switch (item.type) {
2869
+ case "CommandExecution": {
2870
+ const command3 = Array.isArray(item.command) ? item.command.length === 3 && /^-l?c$/.test(item.command[1] ?? "") ? item.command[2] : item.command.join(" ") : unwrapShell(item.command ?? "");
2871
+ const exited = typeof item.exit_code === "number" && item.exit_code !== 0;
2872
+ return { id: item.id, text: "Run a command", detail: shorten(command3), state: exited ? "failed" : state };
2873
+ }
2874
+ case "McpToolCall": {
2875
+ const app = item.server === "codex_apps" && typeof item.appName === "string" && typeof item.actionName === "string";
2876
+ const text = connectorLabel(item.server ?? "tool", app ? `${item.appName}_${item.actionName}` : item.tool ?? "call");
2877
+ return { id: item.id, text, detail: compactArgs(item.arguments), state };
2878
+ }
2879
+ case "Extension": {
2880
+ if (item.kind !== "web.search") return null;
2881
+ const action = item.action ?? {};
2882
+ if (action.type === "openPage") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
2883
+ if (action.type === "findInPage") return { id: item.id, text: "Find in a page", detail: shorten(action.pattern ?? null), state };
2884
+ return { id: item.id, text: "Search the web", detail: shorten(action.query || item.query || null), state };
2885
+ }
2886
+ case "FileChange":
2887
+ return { id: item.id, text: "Write a file", detail: shorten(Object.keys(item.changes ?? {}).join(", ") || null), state };
2888
+ case "ImageView":
2889
+ return { id: item.id, text: "Look at an image", detail: shorten(item.path?.replace(/^file:\/\//, "") ?? null), state };
2890
+ case "CollabAgentToolCall":
2891
+ return item.tool === "wait" ? { id: item.id, text: WAIT, detail: null, state } : null;
2892
+ }
2893
+ return null;
2894
+ }
2895
+
2896
+ // src/agent.ts
2734
2897
  var RUN_TIMEOUT_MS = 60 * 6e4;
2735
2898
  var TURN_TIMEOUT_MS = 10 * 6e4;
2736
2899
  var RUNS_SHOWN = 30;
2900
+ var STEPS_KEPT = 500;
2901
+ var SESSIONS_EVERY_MS = 2e3;
2737
2902
  function agentPrompt(promptPath) {
2738
2903
  return [
2739
2904
  "Run my morning brief for today.",
@@ -2749,6 +2914,11 @@ function agentPrompt(promptPath) {
2749
2914
  "Finish by reporting back as that file asks you to."
2750
2915
  ].join("\n");
2751
2916
  }
2917
+ function agentSettings(config) {
2918
+ return {
2919
+ searchTrash: config.agent.searchTrash
2920
+ };
2921
+ }
2752
2922
  function followUpPrompt(question) {
2753
2923
  return [
2754
2924
  "A follow-up question about the brief you wrote in this session. Answer it here.",
@@ -2759,7 +2929,7 @@ function followUpPrompt(question) {
2759
2929
  question.trim()
2760
2930
  ].join("\n");
2761
2931
  }
2762
- function displayPath(path, home = homedir2()) {
2932
+ function displayPath(path, home = homedir3()) {
2763
2933
  return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
2764
2934
  }
2765
2935
  function foldAgentLog(text) {
@@ -2785,6 +2955,7 @@ function foldAgentLog(text) {
2785
2955
  status: "running",
2786
2956
  report: "",
2787
2957
  messages: [],
2958
+ steps: [],
2788
2959
  error: null,
2789
2960
  turns: [],
2790
2961
  resumable: false
@@ -2825,6 +2996,7 @@ function foldAgentLog(text) {
2825
2996
  run5.sessionId = typeof record.sessionId === "string" ? record.sessionId : run5.sessionId;
2826
2997
  run5.report = typeof record.report === "string" ? record.report : run5.report;
2827
2998
  run5.messages = Array.isArray(record.messages) ? record.messages.filter((message) => typeof message === "string") : run5.report ? [run5.report] : [];
2999
+ run5.steps = readSteps(record.steps);
2828
3000
  run5.endedAt = record.at;
2829
3001
  if (record.event === "finished") {
2830
3002
  run5.status = "done";
@@ -2838,6 +3010,57 @@ function foldAgentLog(text) {
2838
3010
  }
2839
3011
  return [...runs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
2840
3012
  }
3013
+ function readSteps(value) {
3014
+ if (!Array.isArray(value)) return [];
3015
+ const steps = [];
3016
+ for (const candidate of value) {
3017
+ if (typeof candidate !== "object" || candidate === null || typeof candidate.text !== "string") continue;
3018
+ steps.push({
3019
+ kind: candidate.kind === "message" ? "message" : "tool",
3020
+ text: candidate.text,
3021
+ detail: typeof candidate.detail === "string" ? candidate.detail : null,
3022
+ state: candidate.state === "failed" ? "failed" : "done",
3023
+ by: typeof candidate.by === "string" ? candidate.by : null,
3024
+ ...typeof candidate.subagent === "string" ? { subagent: candidate.subagent } : {}
3025
+ });
3026
+ }
3027
+ return steps;
3028
+ }
3029
+ var StepTrail = class {
3030
+ calls = /* @__PURE__ */ new Map();
3031
+ run;
3032
+ constructor(run5) {
3033
+ this.run = run5;
3034
+ }
3035
+ said(by, text) {
3036
+ this.add({ kind: "message", text, detail: null, state: "done", by });
3037
+ }
3038
+ call(by, parsed) {
3039
+ const known = this.calls.get(parsed.id);
3040
+ if (known) {
3041
+ if (parsed.text) known.text = parsed.text;
3042
+ if (parsed.detail) known.detail = parsed.detail;
3043
+ if (!known.subagent || parsed.final || parsed.state === "failed") known.state = parsed.state;
3044
+ return;
3045
+ }
3046
+ if (!parsed.text) return;
3047
+ const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state, by };
3048
+ if (parsed.handoff) step.subagent = parsed.id;
3049
+ this.calls.set(parsed.id, step);
3050
+ this.add(step);
3051
+ }
3052
+ /** A call nobody reported back on went the way the run did. */
3053
+ settle(ok) {
3054
+ for (const step of this.run.steps) if (step.state === "running") step.state = ok ? "done" : "failed";
3055
+ }
3056
+ add(step) {
3057
+ const steps = this.run.steps;
3058
+ steps.push(step);
3059
+ if (steps.length <= STEPS_KEPT) return;
3060
+ const oldest = steps.findIndex((candidate) => !candidate.subagent);
3061
+ steps.splice(oldest === -1 ? 0 : oldest, 1);
3062
+ }
3063
+ };
2841
3064
  var AgentRunner = class {
2842
3065
  /** Every run in the log, oldest first. */
2843
3066
  runs = [];
@@ -2848,11 +3071,14 @@ var AgentRunner = class {
2848
3071
  spawnFn;
2849
3072
  /** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
2850
3073
  codexGate;
2851
- constructor(config, hooks, spawnFn = spawn, execFn) {
3074
+ /** Where Codex writes its session files, which say what a Codex run's subagents do. */
3075
+ codexHome;
3076
+ constructor(config, hooks, spawnFn = spawn, execFn, home = codexHome()) {
2852
3077
  this.config = config;
2853
3078
  this.hooks = hooks;
2854
3079
  this.spawnFn = spawnFn;
2855
3080
  this.codexGate = new CodexGate(config.agent.binPath, execFn);
3081
+ this.codexHome = home;
2856
3082
  }
2857
3083
  get enabled() {
2858
3084
  return this.config.agent.cli !== null;
@@ -2878,7 +3104,7 @@ var AgentRunner = class {
2878
3104
  run5.status = "aborted";
2879
3105
  run5.error = reason;
2880
3106
  run5.endedAt = at;
2881
- await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason, report: run5.report, messages: run5.messages, at });
3107
+ await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason, report: run5.report, messages: run5.messages, steps: run5.steps, at });
2882
3108
  }
2883
3109
  for (const turn of run5.turns) {
2884
3110
  if (turn.status !== "running") continue;
@@ -2898,7 +3124,7 @@ var AgentRunner = class {
2898
3124
  return this.runs.at(-1) ?? null;
2899
3125
  }
2900
3126
  view(now = /* @__PURE__ */ new Date()) {
2901
- const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run5) => ({ ...run5, resumable: this.resumable(run5) }));
3127
+ const runs = this.runs.slice(-RUNS_SHOWN).reverse().map((run5, index) => ({ ...run5, steps: index === 0 ? run5.steps : [], resumable: this.resumable(run5) }));
2902
3128
  return {
2903
3129
  enabled: this.enabled,
2904
3130
  cli: this.config.agent.cli,
@@ -2962,12 +3188,19 @@ var AgentRunner = class {
2962
3188
  status: "running",
2963
3189
  report: "",
2964
3190
  messages: [],
3191
+ steps: [],
2965
3192
  error: null,
2966
3193
  turns: [],
2967
3194
  resumable: false
2968
3195
  };
2969
3196
  this.runs.push(run5);
2970
3197
  await this.append({ event: "started", run: run5.id, cli: cli2, trigger, at: run5.startedAt });
3198
+ try {
3199
+ await writeJsonAtomic(this.config.agentSettingsFile, agentSettings(this.config));
3200
+ } catch (err) {
3201
+ await this.finish(run5, null, { ok: false, error: `could not write ${displayPath(this.config.agentSettingsFile)}: ${err.message}` });
3202
+ return run5;
3203
+ }
2971
3204
  const adapter = ADAPTERS[cli2];
2972
3205
  const args = adapter.args({
2973
3206
  prompt: agentPrompt(displayPath(this.config.promptFile)),
@@ -3067,23 +3300,31 @@ var AgentRunner = class {
3067
3300
  stderr.push(chunk.toString("utf8"));
3068
3301
  if (stderr.length > 50) stderr.shift();
3069
3302
  });
3303
+ const trail = turn ? null : new StepTrail(run5);
3304
+ let reader = null;
3070
3305
  if (child.stdout) {
3071
3306
  const lines = createInterface({ input: child.stdout });
3072
3307
  lines.on("line", (line) => {
3073
3308
  const event = adapter.parse(line);
3074
3309
  if (!event) return;
3075
3310
  if (event.sessionId && !turn) run5.sessionId = event.sessionId;
3311
+ if (trail && cli2 === "codex" && run5.sessionId && !reader) reader = this.readSessions(run5, trail);
3312
+ const by = event.by ?? null;
3076
3313
  if (event.text) {
3077
- if (turn) {
3314
+ if (by !== null) {
3315
+ trail?.said(by, event.text);
3316
+ } else if (turn) {
3078
3317
  turn.reply = turn.reply ? `${turn.reply}
3079
3318
 
3080
3319
  ${event.text}` : event.text;
3081
3320
  } else {
3082
3321
  run5.messages.push(event.text);
3083
3322
  run5.report = event.text;
3323
+ trail?.said(null, event.text);
3084
3324
  }
3085
- this.hooks.onChange();
3086
3325
  }
3326
+ for (const parsed of event.steps ?? []) trail?.call(by, parsed);
3327
+ if (event.text || trail && event.steps) this.hooks.onChange();
3087
3328
  if (event.done) done = event.done;
3088
3329
  });
3089
3330
  }
@@ -3092,14 +3333,53 @@ ${event.text}` : event.text;
3092
3333
  });
3093
3334
  child.on("close", (code) => {
3094
3335
  clearTimeout(timer);
3095
- this.running = null;
3096
3336
  const outcome = running.abortReason ? { ok: false, error: running.abortReason } : done ?? {
3097
3337
  ok: false,
3098
3338
  error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
3099
3339
  };
3100
- void this.finish(run5, turn, outcome, running.abortReason !== null);
3340
+ void (async () => {
3341
+ if (reader) await this.stopReading(reader, trail);
3342
+ this.running = null;
3343
+ trail?.settle(outcome.ok);
3344
+ await this.finish(run5, turn, outcome, running.abortReason !== null);
3345
+ })();
3101
3346
  });
3102
3347
  }
3348
+ /**
3349
+ * Start reading a Codex run's session files for its subagents, now that the
3350
+ * stream has named the run's thread.
3351
+ */
3352
+ readSessions(run5, trail) {
3353
+ const reader = {
3354
+ sessions: new CodexSessions(run5.sessionId, new Date(run5.startedAt), this.codexHome),
3355
+ timer: setInterval(() => void this.readOnce(reader, trail), SESSIONS_EVERY_MS),
3356
+ reading: null
3357
+ };
3358
+ reader.timer.unref();
3359
+ return reader;
3360
+ }
3361
+ readOnce(reader, trail) {
3362
+ reader.reading ??= (async () => {
3363
+ try {
3364
+ const news = await reader.sessions.read();
3365
+ for (const entry of news) {
3366
+ if ("message" in entry) trail.said(entry.by, entry.message);
3367
+ else trail.call(entry.by, entry.step);
3368
+ }
3369
+ if (news.length) this.hooks.onChange();
3370
+ } catch (err) {
3371
+ console.warn(`[daily-focus] could not read Codex's session files: ${err.message}`);
3372
+ } finally {
3373
+ reader.reading = null;
3374
+ }
3375
+ })();
3376
+ return reader.reading;
3377
+ }
3378
+ async stopReading(reader, trail) {
3379
+ clearInterval(reader.timer);
3380
+ await reader.reading;
3381
+ await this.readOnce(reader, trail);
3382
+ }
3103
3383
  /**
3104
3384
  * Kill whatever is going, run or follow-up. What a run already renamed into
3105
3385
  * place stays: the brief it replaced is gone either way, and the archive has it.
@@ -3141,17 +3421,17 @@ ${event.text}` : event.text;
3141
3421
  const report = outcome.reply ?? run5.report;
3142
3422
  run5.report = report;
3143
3423
  run5.endedAt = at;
3144
- const messages = run5.messages;
3424
+ const { messages, steps } = run5;
3145
3425
  if (outcome.ok) {
3146
- await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, at });
3426
+ await this.append({ event: "finished", run: run5.id, sessionId: run5.sessionId, report, messages, steps, at });
3147
3427
  run5.status = "done";
3148
3428
  } else if (aborted) {
3149
3429
  run5.error = outcome.error ?? "stopped";
3150
- await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason: run5.error, report, messages, at });
3430
+ await this.append({ event: "aborted", run: run5.id, sessionId: run5.sessionId, reason: run5.error, report, messages, steps, at });
3151
3431
  run5.status = "aborted";
3152
3432
  } else {
3153
3433
  run5.error = outcome.error ?? "failed";
3154
- await this.append({ event: "failed", run: run5.id, sessionId: run5.sessionId, error: run5.error, report, messages, at });
3434
+ await this.append({ event: "failed", run: run5.id, sessionId: run5.sessionId, error: run5.error, report, messages, steps, at });
3155
3435
  run5.status = "failed";
3156
3436
  }
3157
3437
  this.hooks.onChange();
@@ -3198,19 +3478,19 @@ function watchDataDir(dir, onChange, { debounceMs = 120, ignore = [] } = {}) {
3198
3478
  }
3199
3479
 
3200
3480
  // src/assets.ts
3201
- import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
3202
- import { join as join4 } from "node:path";
3481
+ import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
3482
+ import { join as join5 } from "node:path";
3203
3483
  async function computeAssetVersion(dir) {
3204
3484
  const parts = [];
3205
3485
  async function walk(current) {
3206
3486
  let entries;
3207
3487
  try {
3208
- entries = await readdir2(current, { withFileTypes: true });
3488
+ entries = await readdir3(current, { withFileTypes: true });
3209
3489
  } catch {
3210
3490
  return;
3211
3491
  }
3212
3492
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3213
- const path = join4(current, entry.name);
3493
+ const path = join5(current, entry.name);
3214
3494
  if (entry.isDirectory()) {
3215
3495
  await walk(path);
3216
3496
  } else {
@@ -3270,7 +3550,7 @@ function refusal(req, listenHost) {
3270
3550
  import { spawn as spawn2 } from "node:child_process";
3271
3551
  import { mkdir as mkdir2, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
3272
3552
  import { tmpdir as tmpdir2 } from "node:os";
3273
- import { join as join5 } from "node:path";
3553
+ import { join as join6 } from "node:path";
3274
3554
  import { createInterface as createInterface2 } from "node:readline";
3275
3555
 
3276
3556
  // src/questions.ts
@@ -3871,6 +4151,14 @@ var SETTINGS = [
3871
4151
  choices: EFFORTS,
3872
4152
  fallback: always("The CLI's own")
3873
4153
  },
4154
+ {
4155
+ key: "DAILY_FOCUS_AGENT_SEARCH_TRASH",
4156
+ group: "agent",
4157
+ label: "Search deleted mail",
4158
+ help: "Lets the agent look in your mail\u2019s Trash as well as the inbox and archive. Off, it leaves out what you deleted.",
4159
+ kind: "flag",
4160
+ fallback: always("Off")
4161
+ },
3874
4162
  {
3875
4163
  key: "DAILY_FOCUS_AGENT_TOOLS",
3876
4164
  group: "agent",
@@ -4214,7 +4502,7 @@ async function saveSettings(path, settings) {
4214
4502
  // src/onboarding.ts
4215
4503
  var TURN_TIMEOUT_MS2 = 4 * 6e4;
4216
4504
  var KILL_AFTER_MS = 5e3;
4217
- var STEPS_KEPT = 40;
4505
+ var STEPS_KEPT2 = 40;
4218
4506
  var SESSION_MODEL = {
4219
4507
  claude: { model: "opus", effort: "low" },
4220
4508
  codex: { model: null, effort: "low" }
@@ -4668,8 +4956,8 @@ ${request}`, resume, said);
4668
4956
  return this.settle(question.id, record, { ok: false, error: `Codex could not say which plugins to switch off: ${err.message}` });
4669
4957
  }
4670
4958
  }
4671
- const schemaDir = await mkdtemp(join5(tmpdir2(), "daily-focus-setup-"));
4672
- const schemaFile = join5(schemaDir, "schema.json");
4959
+ const schemaDir = await mkdtemp(join6(tmpdir2(), "daily-focus-setup-"));
4960
+ const schemaFile = join6(schemaDir, "schema.json");
4673
4961
  const json = JSON.stringify(question.agent.schema);
4674
4962
  await writeFile2(schemaFile, json, "utf8");
4675
4963
  await mkdir2(this.config.onboardingDir, { recursive: true });
@@ -4710,7 +4998,7 @@ ${request}`, resume, said);
4710
4998
  const steps = this.steps.get(question.id);
4711
4999
  const add = (step) => {
4712
5000
  steps.push(step);
4713
- if (steps.length > STEPS_KEPT) steps.shift();
5001
+ if (steps.length > STEPS_KEPT2) steps.shift();
4714
5002
  };
4715
5003
  if (child.stdout) {
4716
5004
  createInterface2({ input: child.stdout }).on("line", (line) => {
@@ -5097,7 +5385,7 @@ async function readBody(req) {
5097
5385
  }
5098
5386
  async function serveStatic(res, urlPath) {
5099
5387
  const relative = normalize(decodeURIComponent(urlPath === "/" ? "/index.html" : urlPath));
5100
- const filePath = join6(PUBLIC_DIR, relative);
5388
+ const filePath = join7(PUBLIC_DIR, relative);
5101
5389
  if (filePath !== PUBLIC_DIR && !filePath.startsWith(PUBLIC_DIR + sep)) {
5102
5390
  res.writeHead(403).end("Forbidden");
5103
5391
  return;
@@ -5186,7 +5474,7 @@ async function startServer(env, options = {}) {
5186
5474
  let restart = null;
5187
5475
  let restarting = false;
5188
5476
  let settingsWrites = Promise.resolve();
5189
- await stat4(join6(PUBLIC_DIR, "app.js")).catch(() => {
5477
+ await stat4(join7(PUBLIC_DIR, "app.js")).catch(() => {
5190
5478
  console.warn(
5191
5479
  PACKAGED ? "[daily-focus] public/app.js is missing from the package: reinstall it" : "[daily-focus] public/app.js is missing: run `npm run build` (npm start does so on its own)"
5192
5480
  );
@@ -5373,6 +5661,9 @@ data: ${payload}
5373
5661
  // Appended by this process, which broadcasts on its own.
5374
5662
  basename(config.assistantLogFile),
5375
5663
  basename(config.agentLogFile),
5664
+ // Rewritten by the runner before every run; nothing on the page shows it.
5665
+ basename(config.agentSettingsFile),
5666
+ basename(tempPathFor(config.agentSettingsFile)),
5376
5667
  // Rewritten by the setup screen, which broadcasts on its own.
5377
5668
  basename(config.onboardingFile),
5378
5669
  basename(tempPathFor(config.onboardingFile))
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadConfig
3
- } from "./chunk-4DZEBUPU.js";
3
+ } from "./chunk-DPMHC72S.js";
4
4
  import {
5
5
  IN_NPX_CACHE,
6
6
  PACKAGED,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daily-focus",
3
- "version": "1.2.0-dev.11",
3
+ "version": "1.2.0-dev.13",
4
4
  "description": "An interactive dashboard of what you should focus on today at work",
5
5
  "keywords": [
6
6
  "dashboard",
package/prompts/README.md CHANGED
@@ -36,6 +36,7 @@ alongside `items.schema.json`, so a run only ever names a path inside the store:
36
36
  focus.md you write it, on the setup screen, in the dashboard or by hand
37
37
  sources.md you write it, on the setup screen, in the dashboard or by hand
38
38
  settings.json the dashboard writes: what you set on its settings page
39
+ agent-settings.json the dashboard writes before every run: the settings the agent reads
39
40
  items.json the agent writes it
40
41
  actions.jsonl the dashboard appends
41
42
  agent.jsonl the dashboard appends: every run, its report, and the questions asked of it
@@ -71,6 +72,19 @@ it. Nothing else is handed over: no extra instructions, no checkout, no copy of
71
72
  prompt. Everything that evolves lives in the prompt, in git, where a change to it is
72
73
  reviewable, and the wrapper should never need editing.
73
74
 
75
+ A setting that changes how the agent gathers, such as searching deleted mail, reaches
76
+ it the same way everything else does: through the store. Before each run the dashboard
77
+ writes `agent-settings.json` with the values it is running with, whether they were
78
+ saved on the settings page or set in `.env`, and both prompts say what each key means.
79
+ The wrapper stays the same whatever the settings are.
80
+
81
+ The prompt has the agent hand the reading of its sources to subagents, which share
82
+ its working directory and what its CLI allows it, so the prompt also tells it to
83
+ tell them that they only read. The judging and the writing stay with the agent. The
84
+ run's panel shows each subagent under the step that started it: Claude Code reports
85
+ them in its output, and for Codex the dashboard reads Codex's own session files
86
+ (`src/codexsessions.ts`).
87
+
74
88
  Which days it runs is `DAILY_FOCUS_AGENT_DAYS` — cron-numbered, so `1-5` is Mon–Fri
75
89
  and `0-4` is the Sun–Thu week — and Mon–Fri when unset. The same schedule drives the
76
90
  staleness banner, which turns red once the brief is more than 24 *working* hours old,
@@ -24,6 +24,10 @@ not for you.
24
24
  gather and how to judge it; that file says *who* and *where*.
25
25
  - `items.schema.json` — **you only ever read this.** The machine-readable shape of
26
26
  `items.json`. Validate against it in step 5 if you can.
27
+ - `agent-settings.json` — **you only ever read this.** The choices the user made on
28
+ the dashboard's settings page about how you gather, rewritten by the dashboard
29
+ before every run. Each key is explained where it applies below; a missing file or
30
+ key means that key's default.
27
31
  - `prs.json` — **you only ever read this.** The dashboard's own cache of the pull
28
32
  requests the user has open, refreshed while the dashboard is in use. Facts only —
29
33
  reviews, checks, who last acted — with no judgement attached; use it if it helps,
@@ -100,6 +104,33 @@ Otherwise, each source's window starts three days before the previous brief's
100
104
 
101
105
  ### Step 2 — Gather
102
106
 
107
+ #### Hand the reading to subagents
108
+
109
+ Read the sources in parallel rather than one after another: hand each source to a
110
+ subagent of its own, or a few small ones to one subagent, so the slowest source sets
111
+ the pace rather than the sum of them. Run up to three at a time, starting the next as
112
+ one finishes. The reading is all you hand out. Step 1, the judging in step 3, the
113
+ writing in step 4 and the checks in step 5 stay with you, because they need
114
+ everything in one place.
115
+
116
+ A subagent may not see this file, so give it what it needs in its task: its source's
117
+ section of this step in full, with the value of any setting from `agent-settings.json`
118
+ that the section names; the rules under "Across every source" below; the window from
119
+ step 1; and the ids from the previous brief that came from its source and are still
120
+ open, so it looks at each again. Name each after its source — `calendar`, `email`, `eboks` — since
121
+ the dashboard shows the user which is which while you work.
122
+
123
+ Ask for evidence, not decisions. For each thing that might be an item: its upstream id
124
+ or key, its link, who and what it is about, its dates, and what the source shows about
125
+ whether it is still open. Then what it could not read, why, and the link. Merging
126
+ duplicates, ranking and wording are yours. When an answer is missing something its
127
+ source should have had, ask again or read it yourself.
128
+
129
+ A subagent only reads, and tell it so: it writes no file, `items.json` least of all,
130
+ and changes nothing in any source.
131
+
132
+ If you cannot start subagents, read the sources yourself, one after another.
133
+
103
134
  #### Across every source
104
135
 
105
136
  **Use a connector, API, MCP server or CLI as the evidence source.** `sources.md` says
@@ -148,6 +179,12 @@ and anything filters moved out of the inbox, included. Mail the user filed is no
148
179
  mail they finished with. **Do not limit the search to unread mail**: the things that
149
180
  rot are the ones the user opened, meant to deal with, and didn't.
150
181
 
182
+ Mail the user deleted is mail they finished with, so leave Trash out unless
183
+ `searchTrash` in `agent-settings.json` is `true` (default `false`). A search with no
184
+ `in:` operator already covers archived mail and skips Trash and Spam; when the
185
+ setting is off, don't widen it with `in:anywhere` or a trash option. When it is on,
186
+ search Trash as well, and still never Spam.
187
+
151
188
  **On a first run, search the last 90 days.** The point of that run is to find what
152
189
  fell through the cracks: a reply the user owes, a bill, a form, a booking to confirm,
153
190
  a promise they made in a thread. Afterwards, use the normal window from step 1.
@@ -22,6 +22,10 @@ not for you.
22
22
  how to judge it; that file says *who* and *where*.
23
23
  - `items.schema.json` — **you only ever read this.** The machine-readable shape of
24
24
  `items.json`. Validate against it in step 5 if you can.
25
+ - `agent-settings.json` — **you only ever read this.** The choices the user made on
26
+ the dashboard's settings page about how you gather, rewritten by the dashboard
27
+ before every run. Each key is explained where it applies below; a missing file or
28
+ key means that key's default.
25
29
 
26
30
  There is also an `archive/` directory of dated past briefs. The dashboard maintains
27
31
  it; you never write to it. Read it if you want history — it is how the dashboard
@@ -107,6 +111,33 @@ a recent query alone make unfinished work disappear.
107
111
 
108
112
  ### Step 2 — Gather
109
113
 
114
+ #### Hand the reading to subagents
115
+
116
+ Read the sources in parallel rather than one after another: hand each source to a
117
+ subagent of its own, or a few small ones to one subagent, so the slowest source sets
118
+ the pace rather than the sum of them. Run up to three at a time, starting the next as
119
+ one finishes. The reading is all you hand out. Step 1, the judging in step 3, the
120
+ writing in step 4 and the checks in step 5 stay with you, because they need
121
+ everything in one place.
122
+
123
+ A subagent may not see this file, so give it what it needs in its task: its source's
124
+ section of this step in full, with the value of any setting from `agent-settings.json`
125
+ that the section names; the rules under "Across every source" below; the window from
126
+ step 1; and the ids from the previous brief that came from its source and are still
127
+ open, so it looks at each again. Name each after its source — `calendar`, `gmail`, `github` — since
128
+ the dashboard shows the user which is which while you work.
129
+
130
+ Ask for evidence, not decisions. For each thing that might be an item: its upstream id
131
+ or key, its link, who and what it is about, its dates, and what the source shows about
132
+ whether it is still open. Then what it could not read, why, and the link. Merging
133
+ duplicates, ranking and wording are yours. When an answer is missing something its
134
+ source should have had, ask again or read it yourself.
135
+
136
+ A subagent only reads, and tell it so: it writes no file, `items.json` least of all,
137
+ and changes nothing in any source.
138
+
139
+ If you cannot start subagents, read the sources yourself, one after another.
140
+
110
141
  #### Across every source
111
142
 
112
143
  **Use a connector, API or CLI as the evidence source.** A browser may only help you
@@ -167,6 +198,12 @@ the user's filters moved out of the inbox, included. Mail they filed is not mail
167
198
  finished with, and a label records how they sorted something, not whether they dealt
168
199
  with it.
169
200
 
201
+ Mail the user deleted is mail they finished with, so leave Trash out unless
202
+ `searchTrash` in `agent-settings.json` is `true` (default `false`). A search with no
203
+ `in:` operator already covers archived mail and skips Trash and Spam; when the
204
+ setting is off, don't widen it with `in:anywhere` or a trash option. When it is on,
205
+ search Trash as well, and still never Spam.
206
+
170
207
  Prioritise direct requests, deadlines, decisions, travel and logistics,
171
208
  finance/legal/security, and anyone plausibly waiting on a reply. **Do not limit
172
209
  continuity-sensitive searches to unread mail** — the things that rot are usually the
package/public/app.js CHANGED
@@ -2479,10 +2479,11 @@ function agentRunBanner(run, state2, handlers2) {
2479
2479
  }
2480
2480
  };
2481
2481
  if (run.status === "running") {
2482
+ const helping = (run.steps ?? []).filter((step) => step.subagent && step.state === "running").length;
2482
2483
  return bannerNode(
2483
2484
  "info",
2484
2485
  "i",
2485
- `The morning agent is writing a new brief, started ${relativeTime(run.startedAt, now)}.`,
2486
+ `The morning agent is writing a new brief, started ${relativeTime(run.startedAt, now)}` + (helping ? `, with ${helping} ${helping === 1 ? "subagent" : "subagents"} at work.` : "."),
2486
2487
  [el("button", { type: "button", class: "button banner__action", onClick: () => handlers2.stopAgent() }, "Stop")],
2487
2488
  strip
2488
2489
  );
@@ -3935,6 +3936,7 @@ function describeWhen(value, now) {
3935
3936
  return `${named ? day : date && date.getFullYear() === now.getFullYear() ? formatShortDay(value) : formatDay(value)} at ${formatTime(value)}`;
3936
3937
  }
3937
3938
  function runReport(run, ui2, handlers2) {
3939
+ if (run.steps?.length) return runWork(run, ui2, handlers2);
3938
3940
  const messages = run.messages ?? (run.report ? [run.report] : []);
3939
3941
  const running = run.status === "running";
3940
3942
  const earlier = !running && messages.length && messages.at(-1) === run.report ? messages.slice(0, -1) : messages;
@@ -3993,6 +3995,92 @@ function runReport(run, ui2, handlers2) {
3993
3995
  ) : null
3994
3996
  );
3995
3997
  }
3998
+ function runWork(run, ui2, handlers2) {
3999
+ const running = run.status === "running";
4000
+ if (running) {
4001
+ return el(
4002
+ "section",
4003
+ { class: "flyout__section" },
4004
+ el("h3", { class: "flyout__label" }, "What it is doing"),
4005
+ runSteps(run, run.steps, null, ui2, handlers2)
4006
+ );
4007
+ }
4008
+ const last = run.steps.findLast((step) => step.by === null && step.kind === "message");
4009
+ const steps2 = last && sameWords(last.text, run.report) ? run.steps.filter((step) => step !== last) : run.steps;
4010
+ const handoffs = steps2.filter((step) => step.subagent).length;
4011
+ return el(
4012
+ "section",
4013
+ { class: "flyout__section" },
4014
+ el("h3", { class: "flyout__label" }, run.status === "done" ? "Its report" : "What it said last"),
4015
+ run.report ? el("div", { class: "assistant__reply" }, renderMarkdownBlocks(run.report)) : el("p", { class: "flyout__empty" }, "It said nothing."),
4016
+ steps2.length ? el(
4017
+ "details",
4018
+ { class: "agent-run__list" },
4019
+ el(
4020
+ "summary",
4021
+ null,
4022
+ `How it got there, in ${steps2.length} ${steps2.length === 1 ? "step" : "steps"}` + (handoffs ? `, with ${handoffs} ${handoffs === 1 ? "subagent" : "subagents"}` : "")
4023
+ ),
4024
+ runSteps(run, steps2, null, ui2, handlers2)
4025
+ ) : null
4026
+ );
4027
+ }
4028
+ var NESTING = 4;
4029
+ function runSteps(run, steps2, by, ui2, handlers2, depth = 0) {
4030
+ const own = steps2.filter((step) => step.by === by);
4031
+ return el(
4032
+ "ol",
4033
+ { class: "assistant__steps agent-run__steps" },
4034
+ own.length ? null : el("li", { class: "assistant__step assistant__step--message" }, "Nothing yet."),
4035
+ own.map((step, index) => {
4036
+ if (step.kind === "message") {
4037
+ return el("li", { key: index, class: "agent-run__said" }, renderMarkdownBlocks(step.text));
4038
+ }
4039
+ if (step.subagent && depth < NESTING) return runHandoff(run, steps2, step, ui2, handlers2, depth, index);
4040
+ return el(
4041
+ "li",
4042
+ { key: index, class: "assistant__step", "data-state": step.state },
4043
+ el("span", { class: "assistant__step-text" }, step.text),
4044
+ step.detail ? el("code", { class: "assistant__step-detail", title: step.detail }, step.detail) : null
4045
+ );
4046
+ })
4047
+ );
4048
+ }
4049
+ function runHandoff(run, steps2, step, ui2, handlers2, depth, index) {
4050
+ const id = step.subagent;
4051
+ const own = steps2.filter((candidate) => candidate.by === id);
4052
+ const latest = own.at(-1);
4053
+ const calls = own.filter((candidate) => candidate.kind === "tool").length;
4054
+ const doing = step.state === "running" ? latest ? latest.kind === "message" ? firstLine(latest.text) : latest.text : "starting" : `${calls} ${calls === 1 ? "step" : "steps"}`;
4055
+ const key = `${run.id}:${id}`;
4056
+ const fallback = step.state === "running";
4057
+ const open = ui2.messageOpen.value.has(key) ? ui2.messageOpen.value.get(key) : fallback;
4058
+ return el(
4059
+ "li",
4060
+ { key: index, class: "assistant__step agent-run__handoff", "data-state": step.state },
4061
+ el(
4062
+ "details",
4063
+ {
4064
+ open,
4065
+ // As the message folds do: a toggle back to the default is forgotten.
4066
+ onToggle: (event) => {
4067
+ const isOpen = event.currentTarget.open;
4068
+ handlers2.toggleMessage(key, isOpen === fallback ? null : isOpen);
4069
+ }
4070
+ },
4071
+ el(
4072
+ "summary",
4073
+ null,
4074
+ el("span", { class: "assistant__step-text" }, `Subagent: ${step.detail ?? "unnamed"}`),
4075
+ el("span", { class: "assistant__step-detail" }, doing)
4076
+ ),
4077
+ runSteps(run, steps2, id, ui2, handlers2, depth + 1)
4078
+ )
4079
+ );
4080
+ }
4081
+ function sameWords(a4, b4) {
4082
+ return a4.replace(/\s+/g, " ").trim() === b4.replace(/\s+/g, " ").trim();
4083
+ }
3996
4084
  function runStatus(run, now, handlers2) {
3997
4085
  if (run.status === "running") {
3998
4086
  return el(
package/public/style.css CHANGED
@@ -541,6 +541,64 @@ body[data-view='tickets'] #brief-refresh {
541
541
  .agent-run__message .assistant__reply {
542
542
  padding: 0 10px 8px;
543
543
  }
544
+ /*
545
+ * A run's steps, the assistant's look: what it said in its own words between
546
+ * the calls, and each subagent as a fold under the step that handed it work.
547
+ */
548
+ .agent-run__list > .agent-run__steps {
549
+ margin-top: 8px;
550
+ }
551
+ .agent-run__said {
552
+ margin: 6px 0;
553
+ font-size: 12.5px;
554
+ line-height: 1.45;
555
+ color: var(--ink-1);
556
+ overflow-wrap: anywhere;
557
+ }
558
+ .agent-run__said:first-child {
559
+ margin-top: 0;
560
+ }
561
+ .agent-run__said > * {
562
+ margin: 0;
563
+ }
564
+ .agent-run__said > * + * {
565
+ margin-top: 5px;
566
+ }
567
+ .agent-run__said ul,
568
+ .agent-run__said ol {
569
+ padding-left: 18px;
570
+ }
571
+ .agent-run__said pre {
572
+ white-space: pre-wrap;
573
+ }
574
+ .agent-run__handoff > details {
575
+ flex: 1 1 auto;
576
+ min-width: 0;
577
+ }
578
+ /* The spinner stays on the subagent's own line, not in the middle of its open steps. */
579
+ .agent-run__steps > .agent-run__handoff[data-state='running']::before {
580
+ align-self: flex-start;
581
+ margin-top: 4px;
582
+ }
583
+ .agent-run__handoff summary {
584
+ cursor: pointer;
585
+ overflow: hidden;
586
+ text-overflow: ellipsis;
587
+ white-space: nowrap;
588
+ }
589
+ .agent-run__handoff summary:hover {
590
+ color: var(--ink-1);
591
+ }
592
+ .agent-run__handoff summary .assistant__step-text {
593
+ font-weight: 600;
594
+ }
595
+ .agent-run__handoff summary .assistant__step-detail {
596
+ margin-left: 6px;
597
+ font-size: 12px;
598
+ }
599
+ .agent-run__handoff .agent-run__steps {
600
+ margin-top: 4px;
601
+ }
544
602
  .agent-run__runs {
545
603
  margin: 8px 0 0;
546
604
  padding: 0;