daily-focus 1.2.0-dev.12 → 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.
@@ -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,
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-UKILSZYF.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-RKEB6FDK.js");
69
+ const { runServer } = await import("./server-VPSSCDBW.js");
70
70
  await runServer({
71
71
  ...options,
72
72
  onStarted: (url) => {
@@ -3,7 +3,7 @@ import {
3
3
  actionOf,
4
4
  autoKeys,
5
5
  judge
6
- } from "./chunk-FPBY3TAW.js";
6
+ } from "./chunk-6COKG3VL.js";
7
7
  import {
8
8
  dateFromNow,
9
9
  sampleBrief,
@@ -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) {
@@ -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-FPBY3TAW.js";
15
+ resolveBoard,
16
+ shorten,
17
+ unwrapShell
18
+ } from "./chunk-6COKG3VL.js";
13
19
  import {
14
20
  FOCUS_TEMPLATE,
15
21
  sourcesTemplate
@@ -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.",
@@ -2764,7 +2929,7 @@ function followUpPrompt(question) {
2764
2929
  question.trim()
2765
2930
  ].join("\n");
2766
2931
  }
2767
- function displayPath(path, home = homedir2()) {
2932
+ function displayPath(path, home = homedir3()) {
2768
2933
  return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
2769
2934
  }
2770
2935
  function foldAgentLog(text) {
@@ -2790,6 +2955,7 @@ function foldAgentLog(text) {
2790
2955
  status: "running",
2791
2956
  report: "",
2792
2957
  messages: [],
2958
+ steps: [],
2793
2959
  error: null,
2794
2960
  turns: [],
2795
2961
  resumable: false
@@ -2830,6 +2996,7 @@ function foldAgentLog(text) {
2830
2996
  run5.sessionId = typeof record.sessionId === "string" ? record.sessionId : run5.sessionId;
2831
2997
  run5.report = typeof record.report === "string" ? record.report : run5.report;
2832
2998
  run5.messages = Array.isArray(record.messages) ? record.messages.filter((message) => typeof message === "string") : run5.report ? [run5.report] : [];
2999
+ run5.steps = readSteps(record.steps);
2833
3000
  run5.endedAt = record.at;
2834
3001
  if (record.event === "finished") {
2835
3002
  run5.status = "done";
@@ -2843,6 +3010,57 @@ function foldAgentLog(text) {
2843
3010
  }
2844
3011
  return [...runs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
2845
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
+ };
2846
3064
  var AgentRunner = class {
2847
3065
  /** Every run in the log, oldest first. */
2848
3066
  runs = [];
@@ -2853,11 +3071,14 @@ var AgentRunner = class {
2853
3071
  spawnFn;
2854
3072
  /** Refuses a Codex too old to hold to the connector rules; see `codex.ts`. */
2855
3073
  codexGate;
2856
- 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()) {
2857
3077
  this.config = config;
2858
3078
  this.hooks = hooks;
2859
3079
  this.spawnFn = spawnFn;
2860
3080
  this.codexGate = new CodexGate(config.agent.binPath, execFn);
3081
+ this.codexHome = home;
2861
3082
  }
2862
3083
  get enabled() {
2863
3084
  return this.config.agent.cli !== null;
@@ -2883,7 +3104,7 @@ var AgentRunner = class {
2883
3104
  run5.status = "aborted";
2884
3105
  run5.error = reason;
2885
3106
  run5.endedAt = at;
2886
- 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 });
2887
3108
  }
2888
3109
  for (const turn of run5.turns) {
2889
3110
  if (turn.status !== "running") continue;
@@ -2903,7 +3124,7 @@ var AgentRunner = class {
2903
3124
  return this.runs.at(-1) ?? null;
2904
3125
  }
2905
3126
  view(now = /* @__PURE__ */ new Date()) {
2906
- 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) }));
2907
3128
  return {
2908
3129
  enabled: this.enabled,
2909
3130
  cli: this.config.agent.cli,
@@ -2967,6 +3188,7 @@ var AgentRunner = class {
2967
3188
  status: "running",
2968
3189
  report: "",
2969
3190
  messages: [],
3191
+ steps: [],
2970
3192
  error: null,
2971
3193
  turns: [],
2972
3194
  resumable: false
@@ -3078,23 +3300,31 @@ var AgentRunner = class {
3078
3300
  stderr.push(chunk.toString("utf8"));
3079
3301
  if (stderr.length > 50) stderr.shift();
3080
3302
  });
3303
+ const trail = turn ? null : new StepTrail(run5);
3304
+ let reader = null;
3081
3305
  if (child.stdout) {
3082
3306
  const lines = createInterface({ input: child.stdout });
3083
3307
  lines.on("line", (line) => {
3084
3308
  const event = adapter.parse(line);
3085
3309
  if (!event) return;
3086
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;
3087
3313
  if (event.text) {
3088
- if (turn) {
3314
+ if (by !== null) {
3315
+ trail?.said(by, event.text);
3316
+ } else if (turn) {
3089
3317
  turn.reply = turn.reply ? `${turn.reply}
3090
3318
 
3091
3319
  ${event.text}` : event.text;
3092
3320
  } else {
3093
3321
  run5.messages.push(event.text);
3094
3322
  run5.report = event.text;
3323
+ trail?.said(null, event.text);
3095
3324
  }
3096
- this.hooks.onChange();
3097
3325
  }
3326
+ for (const parsed of event.steps ?? []) trail?.call(by, parsed);
3327
+ if (event.text || trail && event.steps) this.hooks.onChange();
3098
3328
  if (event.done) done = event.done;
3099
3329
  });
3100
3330
  }
@@ -3103,14 +3333,53 @@ ${event.text}` : event.text;
3103
3333
  });
3104
3334
  child.on("close", (code) => {
3105
3335
  clearTimeout(timer);
3106
- this.running = null;
3107
3336
  const outcome = running.abortReason ? { ok: false, error: running.abortReason } : done ?? {
3108
3337
  ok: false,
3109
3338
  error: `${cli2} exited with code ${code}${stderr.length ? `: ${stderr.join("").trim().split("\n").slice(-3).join(" ")}` : ""}`
3110
3339
  };
3111
- 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
+ })();
3112
3346
  });
3113
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
+ }
3114
3383
  /**
3115
3384
  * Kill whatever is going, run or follow-up. What a run already renamed into
3116
3385
  * place stays: the brief it replaced is gone either way, and the archive has it.
@@ -3152,17 +3421,17 @@ ${event.text}` : event.text;
3152
3421
  const report = outcome.reply ?? run5.report;
3153
3422
  run5.report = report;
3154
3423
  run5.endedAt = at;
3155
- const messages = run5.messages;
3424
+ const { messages, steps } = run5;
3156
3425
  if (outcome.ok) {
3157
- 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 });
3158
3427
  run5.status = "done";
3159
3428
  } else if (aborted) {
3160
3429
  run5.error = outcome.error ?? "stopped";
3161
- 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 });
3162
3431
  run5.status = "aborted";
3163
3432
  } else {
3164
3433
  run5.error = outcome.error ?? "failed";
3165
- 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 });
3166
3435
  run5.status = "failed";
3167
3436
  }
3168
3437
  this.hooks.onChange();
@@ -3209,19 +3478,19 @@ function watchDataDir(dir, onChange, { debounceMs = 120, ignore = [] } = {}) {
3209
3478
  }
3210
3479
 
3211
3480
  // src/assets.ts
3212
- import { readdir as readdir2, stat as stat3 } from "node:fs/promises";
3213
- 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";
3214
3483
  async function computeAssetVersion(dir) {
3215
3484
  const parts = [];
3216
3485
  async function walk(current) {
3217
3486
  let entries;
3218
3487
  try {
3219
- entries = await readdir2(current, { withFileTypes: true });
3488
+ entries = await readdir3(current, { withFileTypes: true });
3220
3489
  } catch {
3221
3490
  return;
3222
3491
  }
3223
3492
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3224
- const path = join4(current, entry.name);
3493
+ const path = join5(current, entry.name);
3225
3494
  if (entry.isDirectory()) {
3226
3495
  await walk(path);
3227
3496
  } else {
@@ -3281,7 +3550,7 @@ function refusal(req, listenHost) {
3281
3550
  import { spawn as spawn2 } from "node:child_process";
3282
3551
  import { mkdir as mkdir2, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
3283
3552
  import { tmpdir as tmpdir2 } from "node:os";
3284
- import { join as join5 } from "node:path";
3553
+ import { join as join6 } from "node:path";
3285
3554
  import { createInterface as createInterface2 } from "node:readline";
3286
3555
 
3287
3556
  // src/questions.ts
@@ -4233,7 +4502,7 @@ async function saveSettings(path, settings) {
4233
4502
  // src/onboarding.ts
4234
4503
  var TURN_TIMEOUT_MS2 = 4 * 6e4;
4235
4504
  var KILL_AFTER_MS = 5e3;
4236
- var STEPS_KEPT = 40;
4505
+ var STEPS_KEPT2 = 40;
4237
4506
  var SESSION_MODEL = {
4238
4507
  claude: { model: "opus", effort: "low" },
4239
4508
  codex: { model: null, effort: "low" }
@@ -4687,8 +4956,8 @@ ${request}`, resume, said);
4687
4956
  return this.settle(question.id, record, { ok: false, error: `Codex could not say which plugins to switch off: ${err.message}` });
4688
4957
  }
4689
4958
  }
4690
- const schemaDir = await mkdtemp(join5(tmpdir2(), "daily-focus-setup-"));
4691
- const schemaFile = join5(schemaDir, "schema.json");
4959
+ const schemaDir = await mkdtemp(join6(tmpdir2(), "daily-focus-setup-"));
4960
+ const schemaFile = join6(schemaDir, "schema.json");
4692
4961
  const json = JSON.stringify(question.agent.schema);
4693
4962
  await writeFile2(schemaFile, json, "utf8");
4694
4963
  await mkdir2(this.config.onboardingDir, { recursive: true });
@@ -4729,7 +4998,7 @@ ${request}`, resume, said);
4729
4998
  const steps = this.steps.get(question.id);
4730
4999
  const add = (step) => {
4731
5000
  steps.push(step);
4732
- if (steps.length > STEPS_KEPT) steps.shift();
5001
+ if (steps.length > STEPS_KEPT2) steps.shift();
4733
5002
  };
4734
5003
  if (child.stdout) {
4735
5004
  createInterface2({ input: child.stdout }).on("line", (line) => {
@@ -5116,7 +5385,7 @@ async function readBody(req) {
5116
5385
  }
5117
5386
  async function serveStatic(res, urlPath) {
5118
5387
  const relative = normalize(decodeURIComponent(urlPath === "/" ? "/index.html" : urlPath));
5119
- const filePath = join6(PUBLIC_DIR, relative);
5388
+ const filePath = join7(PUBLIC_DIR, relative);
5120
5389
  if (filePath !== PUBLIC_DIR && !filePath.startsWith(PUBLIC_DIR + sep)) {
5121
5390
  res.writeHead(403).end("Forbidden");
5122
5391
  return;
@@ -5205,7 +5474,7 @@ async function startServer(env, options = {}) {
5205
5474
  let restart = null;
5206
5475
  let restarting = false;
5207
5476
  let settingsWrites = Promise.resolve();
5208
- await stat4(join6(PUBLIC_DIR, "app.js")).catch(() => {
5477
+ await stat4(join7(PUBLIC_DIR, "app.js")).catch(() => {
5209
5478
  console.warn(
5210
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)"
5211
5480
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daily-focus",
3
- "version": "1.2.0-dev.12",
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
@@ -78,6 +78,13 @@ writes `agent-settings.json` with the values it is running with, whether they we
78
78
  saved on the settings page or set in `.env`, and both prompts say what each key means.
79
79
  The wrapper stays the same whatever the settings are.
80
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
+
81
88
  Which days it runs is `DAILY_FOCUS_AGENT_DAYS` — cron-numbered, so `1-5` is Mon–Fri
82
89
  and `0-4` is the Sun–Thu week — and Mon–Fri when unset. The same schedule drives the
83
90
  staleness banner, which turns red once the brief is more than 24 *working* hours old,
@@ -104,6 +104,33 @@ Otherwise, each source's window starts three days before the previous brief's
104
104
 
105
105
  ### Step 2 — Gather
106
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
+
107
134
  #### Across every source
108
135
 
109
136
  **Use a connector, API, MCP server or CLI as the evidence source.** `sources.md` says
@@ -111,6 +111,33 @@ a recent query alone make unfinished work disappear.
111
111
 
112
112
  ### Step 2 — Gather
113
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
+
114
141
  #### Across every source
115
142
 
116
143
  **Use a connector, API or CLI as the evidence source.** A browser may only help you
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;