daily-focus 1.2.0-dev.7 → 1.2.0-dev.9

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
@@ -64,13 +64,6 @@ since you completed something that advanced your objective.
64
64
  - Mark tasks **Done** when you've handled them.
65
65
  - **Snooze** an item until a date, or **Dismiss** it if it doesn't need attention.
66
66
  - Leave a **Note** for the next brief, such as "already replied on Slack".
67
- - Press **f** for focus mode, which shows your top item and agenda.
68
- - Press **p** on an item to start a focus timer. Its countdown appears in the tab
69
- title, and the session becomes part of the next brief's context.
70
-
71
- The timer keeps counting after its target time. On macOS, it ends a session when
72
- you've been away from the machine, excluding the time you were away. On systems
73
- without idle detection, a two-hour limit caps the recorded session.
74
67
 
75
68
  You can undo item actions except notes. The page updates as new data arrives, and
76
69
  the theme button switches between system, light, and dark mode.
@@ -137,9 +130,13 @@ why is CI red, what does this thread actually want from me. With
137
130
  `DAILY_FOCUS_ASSISTANT` set to `claude` or `codex`, every item's panel gets an
138
131
  assistant that runs that CLI headless against the item, with quick actions for the
139
132
  common asks and a field for anything else. Click a card to open its panel, where its
140
- notes live too; the reply streams in there, and you can follow up in the same
141
- conversation. The panel stays on the item it was opened for while you move through
142
- the list.
133
+ notes live too. While the assistant works, the panel shows what it is doing: each
134
+ command it runs and each page, thread or ticket it reads, with a clock, and a cross
135
+ on anything that failed, such as a repository it couldn't see. Once it answers,
136
+ that trail folds away under the answer. The answer has buttons to copy it, or any
137
+ block in it meant for pasting, and to open a draft it created. The field stays at
138
+ the foot of the panel, so you can follow up in the same conversation. The panel
139
+ stays on the item it was opened for while you move through the list.
143
140
 
144
141
  It is told to read and draft, and nothing more. An email reply is created as a Gmail
145
142
  draft on the thread, so it is waiting there when you open the mail; a reply to a
@@ -177,8 +174,6 @@ Press **?** in the dashboard to see the shortcuts.
177
174
  | `a` | Open the panel, ready to ask the assistant |
178
175
  | `o` | Open the source link |
179
176
  | `u` | Undo the last action |
180
- | `f` | Toggle focus mode |
181
- | `p` | Start / stop a focus session |
182
177
  | `r` | Refresh the current board |
183
178
  | `w` | Switch Jira views |
184
179
  | `,` | Settings |
package/SETUP.md CHANGED
@@ -148,8 +148,8 @@ Set `DAILY_FOCUS_AGENT_MODEL` and `DAILY_FOCUS_AGENT_EFFORT` if the CLI's own
148
148
  defaults are not what you want for a brief; a brief is worth a high effort. If the
149
149
  server can't find the CLI, set `DAILY_FOCUS_AGENT_BIN`.
150
150
 
151
- The agent writes `items.json`. The dashboard records your actions and focus sessions
152
- for the next run to read.
151
+ The agent writes `items.json`. The dashboard records your actions for the next run
152
+ to read.
153
153
 
154
154
  After the first run, read its report (click "updated … ago"): it says which sources
155
155
  it couldn't reach. To check the brief against the payload contract, run:
@@ -461,7 +461,7 @@ DAILY_FOCUS_PROFILE=personal
461
461
  The dashboard then links the personal prompt, `prompts/morning-brief-personal.md`,
462
462
  into the store as `prompt.md`, and the source list starts from a template with
463
463
  sections for those sources. The profile also switches off
464
- the Jira board and away detection, and gives the tab a green house icon instead of the blue briefcase. The pull request
464
+ the Jira board, and gives the tab a green house icon instead of the blue briefcase. The pull request
465
465
  board stays on for side projects; point it at your personal account with
466
466
  `DAILY_FOCUS_GITHUB_ACCOUNTS`, or set `DAILY_FOCUS_GITHUB=off`.
467
467
 
@@ -522,8 +522,6 @@ matters when `DAILY_FOCUS_FREE_WINDOWS` is off.
522
522
  | `DAILY_FOCUS_MIN_FREE_WINDOW` | `45` | Minutes before a gap counts as a focus window |
523
523
  | `DAILY_FOCUS_STALE_AFTER_HOURS` | `24` | When to expect a refresh. Shows an informational message for 45 minutes before warning that the agent may not have run. Counted only in hours a run was due, so days off never trip it |
524
524
  | `DAILY_FOCUS_AGENT_DAYS` | `1-5` | Weekdays the dashboard runs the agent on, cron-style and cron-numbered: `1-5` for Monday to Friday, `0-4` for Sunday to Thursday, `0,6` for a weekend-only run |
525
- | `DAILY_FOCUS_SESSION_MINUTES` | `25` | Default focus session length |
526
- | `DAILY_FOCUS_AWAY_AFTER` | `10`; `0` for personal | Minutes of an untouched machine before a session is closed at the last sign of life. `0` turns it off |
527
525
  | `DAILY_FOCUS_GITHUB` | `on` | `off` disables the pull request board, and the ticket board stops checking GitHub; nothing is polled |
528
526
  | `DAILY_FOCUS_GITHUB_ACCOUNTS` | *gh's active account* | Logins to poll as, comma-separated, each resolved with `gh auth token --user` |
529
527
  | `DAILY_FOCUS_GITHUB_SCOPE` | *everything* | Organisations or `owner/repo` entries to limit the board to, comma-separated |
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  readSystemZone,
3
3
  startingZone
4
- } from "./chunk-4DCBCR4D.js";
4
+ } from "./chunk-6SMUSPZG.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-JQGYCWRX.js";
16
+ } from "./chunk-GQ3NCWRT.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-RNE27FQY.js";
26
+ } from "./chunk-KZNY45XW.js";
27
27
  import {
28
28
  command
29
29
  } from "./chunk-BC2W7NND.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  writeJsonAtomic
3
- } from "./chunk-JQGYCWRX.js";
3
+ } from "./chunk-GQ3NCWRT.js";
4
4
  import {
5
5
  localTimeZone
6
- } from "./chunk-RNE27FQY.js";
6
+ } from "./chunk-KZNY45XW.js";
7
7
 
8
8
  // src/zone.ts
9
9
  import { readFile, readlink } from "node:fs/promises";
@@ -14,7 +14,7 @@ import {
14
14
  startOfLocalDay,
15
15
  workingDaysSince,
16
16
  workingMsBetween
17
- } from "./chunk-RNE27FQY.js";
17
+ } from "./chunk-KZNY45XW.js";
18
18
 
19
19
  // src/fs.ts
20
20
  import { mkdir, rename, writeFile } from "node:fs/promises";
@@ -390,172 +390,9 @@ function findLeakedPhrase(brief, focus) {
390
390
  return null;
391
391
  }
392
392
 
393
- // src/sessions.ts
394
- import { appendFile, mkdir as mkdir2, readFile as readFile2, unlink } from "node:fs/promises";
395
- var MAX_SESSION_MINUTES = 120;
396
- var MIN_LOGGABLE_MINUTES = 1;
397
- async function readActive(config) {
398
- try {
399
- return JSON.parse(await readFile2(config.sessionFile, "utf8"));
400
- } catch {
401
- return null;
402
- }
403
- }
404
- async function writeActive(config, session) {
405
- await writeJsonAtomic(config.sessionFile, session);
406
- }
407
- async function clearActive(config) {
408
- try {
409
- await unlink(config.sessionFile);
410
- } catch {
411
- }
412
- }
413
- async function logSession(config, session, endedAt, endedBy) {
414
- const startedAt = parseISO(session.startedAt);
415
- if (!startedAt) return null;
416
- const actualMinutes = Math.round((endedAt.getTime() - startedAt.getTime()) / 6e4);
417
- if (actualMinutes < MIN_LOGGABLE_MINUTES) return null;
418
- const entry = {
419
- id: session.id,
420
- title: session.title,
421
- startedAt: session.startedAt,
422
- endedAt: endedAt.toISOString(),
423
- plannedMinutes: session.minutes,
424
- actualMinutes,
425
- reachedTarget: actualMinutes >= session.minutes,
426
- advancesObjective: session.advancesObjective,
427
- endedBy
428
- };
429
- await mkdir2(config.dataDir, { recursive: true });
430
- await appendFile(config.sessionsLogFile, `${JSON.stringify(entry)}
431
- `, "utf8");
432
- return entry;
433
- }
434
- async function startSession(config, opts) {
435
- const now = opts.now ?? /* @__PURE__ */ new Date();
436
- await stopSession(config, now);
437
- await writeActive(config, {
438
- id: opts.id,
439
- title: opts.title,
440
- startedAt: now.toISOString(),
441
- minutes: opts.minutes,
442
- advancesObjective: opts.advancesObjective,
443
- // The click that got here is itself the first evidence of a person.
444
- lastActiveAt: now.toISOString()
445
- });
446
- }
447
- async function stopSession(config, now = /* @__PURE__ */ new Date(), endedBy = "user") {
448
- const active = await readActive(config);
449
- if (!active) return null;
450
- await logSession(config, active, now, endedBy);
451
- await clearActive(config);
452
- return active;
453
- }
454
- async function reconcileSession(config, probe, now = /* @__PURE__ */ new Date()) {
455
- if (config.awayAfterMinutes <= 0) return null;
456
- const active = await readActive(config);
457
- if (!active) return null;
458
- const startedAt = parseISO(active.startedAt);
459
- if (!startedAt) {
460
- await clearActive(config);
461
- return null;
462
- }
463
- const awayAfterMs = config.awayAfterMinutes * 6e4;
464
- const previous = parseISO(active.lastActiveAt) ?? startedAt;
465
- if (now.getTime() - previous.getTime() > awayAfterMs) {
466
- const logged = await logSession(config, active, previous, "away");
467
- await clearActive(config);
468
- return logged;
469
- }
470
- const idleSeconds = await probe();
471
- const lastInput = idleSeconds === null ? now : new Date(now.getTime() - Math.max(0, idleSeconds) * 1e3);
472
- const confirmed = lastInput.getTime() > previous.getTime() ? lastInput : previous;
473
- if (confirmed.toISOString() !== active.lastActiveAt) {
474
- await writeActive(config, { ...active, lastActiveAt: confirmed.toISOString() });
475
- }
476
- return null;
477
- }
478
- async function closeIfPastBackstop(config, active, now) {
479
- if (!active) return null;
480
- const startedAt = parseISO(active.startedAt);
481
- const elapsed = startedAt ? (now.getTime() - startedAt.getTime()) / 6e4 : Infinity;
482
- if (elapsed <= MAX_SESSION_MINUTES) return active;
483
- const ceiling = new Date((startedAt?.getTime() ?? now.getTime()) + MAX_SESSION_MINUTES * 6e4);
484
- await stopSession(config, ceiling, "limit");
485
- return null;
486
- }
487
- function lastUnattendedClose(todayLog) {
488
- const last = todayLog.at(-1);
489
- if (!last || last.endedBy === void 0 || last.endedBy === "user") return null;
490
- return {
491
- id: last.id,
492
- title: last.title,
493
- endedAt: last.endedAt,
494
- actualMinutes: last.actualMinutes,
495
- reason: last.endedBy
496
- };
497
- }
498
- async function readSessionState(config, agenda, now = /* @__PURE__ */ new Date()) {
499
- const active = await closeIfPastBackstop(config, await readActive(config), now);
500
- const todayLog = await readTodaysSessions(config, now);
501
- const completedToday = todayLog.length;
502
- const minutesToday = todayLog.reduce((sum, entry) => sum + entry.actualMinutes, 0);
503
- const unattendedClose = active ? null : lastUnattendedClose(todayLog);
504
- if (!active) {
505
- return { active: null, completedToday, minutesToday, unattendedClose };
506
- }
507
- const startedAt = parseISO(active.startedAt);
508
- const endsAt = new Date(startedAt.getTime() + active.minutes * 6e4);
509
- const remainingSeconds = Math.round((endsAt.getTime() - now.getTime()) / 1e3);
510
- const nextEvent = agenda.events.map((event) => parseISO(event.start)).filter((start) => start !== null && start.getTime() > now.getTime()).sort((a, b) => a.getTime() - b.getTime())[0];
511
- return {
512
- active: {
513
- id: active.id,
514
- title: active.title,
515
- startedAt: active.startedAt,
516
- advancesObjective: active.advancesObjective,
517
- lastActiveAt: active.lastActiveAt,
518
- endsAt: endsAt.toISOString(),
519
- minutes: active.minutes,
520
- remainingSeconds,
521
- overrun: remainingSeconds < 0,
522
- collidesWithNextEvent: nextEvent ? nextEvent.getTime() < endsAt.getTime() : false,
523
- nextEventAt: nextEvent ? nextEvent.toISOString() : null
524
- },
525
- completedToday,
526
- minutesToday,
527
- unattendedClose
528
- };
529
- }
530
- async function readSessions(config) {
531
- let text;
532
- try {
533
- text = await readFile2(config.sessionsLogFile, "utf8");
534
- } catch {
535
- return [];
536
- }
537
- const sessions = [];
538
- for (const line of text.split("\n")) {
539
- if (line.trim() === "") continue;
540
- try {
541
- const entry = JSON.parse(line);
542
- if (entry?.id && entry.startedAt) sessions.push(entry);
543
- } catch {
544
- }
545
- }
546
- return sessions;
547
- }
548
- async function readTodaysSessions(config, now) {
549
- const all = await readSessions(config);
550
- return all.filter((entry) => {
551
- const started = parseISO(entry.startedAt);
552
- return started !== null && started.getFullYear() === now.getFullYear() && started.getMonth() === now.getMonth() && started.getDate() === now.getDate();
553
- });
554
- }
555
-
556
393
  // src/store.ts
557
394
  import { createHash } from "node:crypto";
558
- import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile3, stat } from "node:fs/promises";
395
+ import { appendFile, mkdir as mkdir2, readFile as readFile2, stat } from "node:fs/promises";
559
396
  import { setTimeout as delay } from "node:timers/promises";
560
397
 
561
398
  // src/agenda.ts
@@ -712,7 +549,7 @@ var Store = class {
712
549
  this.config = config;
713
550
  }
714
551
  async ensureDataDir() {
715
- await mkdir3(this.config.dataDir, { recursive: true });
552
+ await mkdir2(this.config.dataDir, { recursive: true });
716
553
  }
717
554
  /**
718
555
  * Which store this is, as an opaque fingerprint: its path, and the directory's
@@ -740,7 +577,7 @@ var Store = class {
740
577
  for (let attempt = 0; attempt < READ_RETRIES; attempt++) {
741
578
  let text;
742
579
  try {
743
- text = await readFile3(this.config.itemsFile, "utf8");
580
+ text = await readFile2(this.config.itemsFile, "utf8");
744
581
  } catch (err) {
745
582
  if (err.code === "ENOENT") {
746
583
  return {
@@ -761,7 +598,7 @@ var Store = class {
761
598
  async readActions() {
762
599
  let text;
763
600
  try {
764
- text = await readFile3(this.config.actionsFile, "utf8");
601
+ text = await readFile2(this.config.actionsFile, "utf8");
765
602
  } catch (err) {
766
603
  if (err.code === "ENOENT") return [];
767
604
  throw err;
@@ -793,7 +630,7 @@ var Store = class {
793
630
  /** Read the standing objective. Returns null when focus.md hasn't been written. */
794
631
  async readFocus() {
795
632
  try {
796
- return parseFocus(await readFile3(this.config.focusFile, "utf8"));
633
+ return parseFocus(await readFile2(this.config.focusFile, "utf8"));
797
634
  } catch (err) {
798
635
  if (err.code === "ENOENT") return null;
799
636
  console.warn(`[daily-focus] could not read focus.md: ${err.message}`);
@@ -806,7 +643,7 @@ var Store = class {
806
643
  `;
807
644
  const write = this.#writeQueue.then(async () => {
808
645
  await this.ensureDataDir();
809
- await appendFile2(this.config.actionsFile, line, "utf8");
646
+ await appendFile(this.config.actionsFile, line, "utf8");
810
647
  });
811
648
  this.#writeQueue = write.catch(() => {
812
649
  });
@@ -874,12 +711,10 @@ var Store = class {
874
711
  }
875
712
  objectiveProgress = computeObjectiveProgress(withToday, actions, now, isRunDay);
876
713
  }
877
- const session = await readSessionState(this.config, agenda, now);
878
714
  return {
879
715
  // toPublicFocus, not focus — the agent-only section must never be serialised.
880
716
  focus: focus ? toPublicFocus(focus) : null,
881
717
  objectiveProgress,
882
- session,
883
718
  schedule: {
884
719
  days: [...schedule.days],
885
720
  source: schedule.source,
@@ -998,9 +833,6 @@ export {
998
833
  setsStatus,
999
834
  readArchiveIndex,
1000
835
  runContractChecks,
1001
- startSession,
1002
- stopSession,
1003
- reconcileSession,
1004
836
  Store,
1005
837
  foldActionLog,
1006
838
  resolveItems
@@ -107,6 +107,8 @@ import { appendFile, mkdir, readFile } from "node:fs/promises";
107
107
  import { createInterface } from "node:readline";
108
108
  var TURN_TIMEOUT_MS = 10 * 6e4;
109
109
  var TURNS_SHOWN = 20;
110
+ var STEPS_KEPT = 100;
111
+ var DETAIL_LENGTH = 200;
110
112
  var QUICK_ACTIONS = [
111
113
  {
112
114
  id: "assess-review",
@@ -193,6 +195,7 @@ function foldAssistantLog(text) {
193
195
  endedAt: null,
194
196
  status: "running",
195
197
  reply: "",
198
+ steps: [],
196
199
  error: null
197
200
  });
198
201
  continue;
@@ -201,6 +204,7 @@ function foldAssistantLog(text) {
201
204
  if (!turn) continue;
202
205
  turn.sessionId = typeof record.sessionId === "string" ? record.sessionId : turn.sessionId;
203
206
  turn.reply = typeof record.reply === "string" ? record.reply : turn.reply;
207
+ turn.steps = readSteps(record.steps);
204
208
  turn.endedAt = record.at;
205
209
  if (record.event === "finished") {
206
210
  turn.status = "done";
@@ -214,6 +218,27 @@ function foldAssistantLog(text) {
214
218
  }
215
219
  return turns;
216
220
  }
221
+ function addStep(turn, step) {
222
+ turn.steps.push(step);
223
+ if (turn.steps.length > STEPS_KEPT) turn.steps.shift();
224
+ }
225
+ function sameWords(a, b) {
226
+ return a.replace(/\s+/g, " ").trim() === b.replace(/\s+/g, " ").trim();
227
+ }
228
+ function readSteps(value) {
229
+ if (!Array.isArray(value)) return [];
230
+ const steps = [];
231
+ for (const candidate of value) {
232
+ if (typeof candidate !== "object" || candidate === null || typeof candidate.text !== "string") continue;
233
+ steps.push({
234
+ kind: candidate.kind === "message" ? "message" : "tool",
235
+ text: candidate.text,
236
+ detail: typeof candidate.detail === "string" ? candidate.detail : null,
237
+ state: candidate.state === "failed" ? "failed" : "done"
238
+ });
239
+ }
240
+ return steps;
241
+ }
217
242
  function parseLine(line) {
218
243
  const trimmed = line.trim();
219
244
  if (!trimmed.startsWith("{")) return null;
@@ -251,11 +276,15 @@ var claudeAdapter = {
251
276
  if (type === "system" && typeof event.session_id === "string") {
252
277
  return { sessionId: event.session_id };
253
278
  }
254
- if (type === "assistant") {
279
+ if (type === "assistant" || type === "user") {
255
280
  const message = event.message;
256
281
  const content = Array.isArray(message?.content) ? message.content : [];
257
- const text = content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n");
258
- return text ? { text } : null;
282
+ const parsed = {};
283
+ const text = type === "assistant" ? content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n\n") : "";
284
+ if (text) parsed.text = text;
285
+ const steps = content.flatMap((block) => claudeStep(block) ?? []);
286
+ if (steps.length) parsed.steps = steps;
287
+ return text || steps.length ? parsed : null;
259
288
  }
260
289
  if (type === "result") {
261
290
  const ok = event.is_error !== true && event.subtype === "success";
@@ -293,10 +322,11 @@ var codexAdapter = {
293
322
  if (type === "thread.started" && typeof event.thread_id === "string") {
294
323
  return { sessionId: event.thread_id };
295
324
  }
296
- if (type === "item.completed") {
325
+ if (type === "item.started" || type === "item.completed") {
297
326
  const item = event.item;
298
- if (item?.type === "agent_message" && typeof item.text === "string") return { text: item.text };
299
- return null;
327
+ if (type === "item.completed" && item?.type === "agent_message" && typeof item.text === "string") return { text: item.text };
328
+ const step = item ? codexStep(item, type === "item.completed") : null;
329
+ return step ? { steps: [step] } : null;
300
330
  }
301
331
  if (type === "turn.completed") return { done: { ok: true } };
302
332
  if (type === "turn.failed" || type === "error") {
@@ -311,6 +341,87 @@ var ADAPTERS = {
311
341
  claude: claudeAdapter,
312
342
  codex: codexAdapter
313
343
  };
344
+ var CLAUDE_QUIET_TOOLS = /* @__PURE__ */ new Set(["ToolSearch", "TodoWrite"]);
345
+ function claudeStep(block) {
346
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
347
+ return { id: block.tool_use_id, state: block.is_error === true ? "failed" : "done" };
348
+ }
349
+ if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string") return null;
350
+ if (CLAUDE_QUIET_TOOLS.has(block.name)) return null;
351
+ const input = block.input ?? {};
352
+ const said = (key) => typeof input[key] === "string" && input[key] !== "" ? input[key] : null;
353
+ const step = (text, detail) => ({ id: block.id, text, detail: shorten(detail), state: "running" });
354
+ switch (block.name) {
355
+ case "Bash":
356
+ return step(said("description") ?? "Run a command", said("command"));
357
+ case "WebFetch":
358
+ return step("Fetch a page", said("url"));
359
+ case "WebSearch":
360
+ return step("Search the web", said("query"));
361
+ case "Read":
362
+ return step("Read a file", said("file_path"));
363
+ case "Grep":
364
+ return step("Search files", said("pattern"));
365
+ case "Glob":
366
+ return step("List files", said("pattern"));
367
+ case "Task":
368
+ case "Agent":
369
+ return step("Hand part of it to a subagent", said("description"));
370
+ }
371
+ const mcp = /^mcp__(.+?)__(.+)$/.exec(block.name);
372
+ if (mcp) return step(connectorLabel(mcp[1], mcp[2]), compactArgs(input));
373
+ return step(block.name, compactArgs(input));
374
+ }
375
+ function codexStep(item, completed) {
376
+ if (typeof item.id !== "string") return null;
377
+ const failed = item.status === "failed" || item.error !== void 0 && item.error !== null;
378
+ const state = completed ? failed ? "failed" : "done" : "running";
379
+ if (item.type === "command_execution") {
380
+ const exited = typeof item.exit_code === "number" && item.exit_code !== 0;
381
+ return {
382
+ id: item.id,
383
+ text: "Run a command",
384
+ detail: shorten(unwrapShell(item.command ?? "")),
385
+ state: completed && exited ? "failed" : state
386
+ };
387
+ }
388
+ if (item.type === "mcp_tool_call" && typeof item.server === "string" && typeof item.tool === "string") {
389
+ return { id: item.id, text: connectorLabel(item.server, item.tool), detail: compactArgs(item.arguments), state };
390
+ }
391
+ if (item.type === "web_search") {
392
+ const action = item.action ?? {};
393
+ if (action.type === "open_page") return { id: item.id, text: "Open a page", detail: shorten(action.url ?? null), state };
394
+ if (action.type === "find_in_page") return { id: item.id, text: "Find in a page", detail: shorten(action.pattern ?? action.url ?? null), state };
395
+ return { id: item.id, text: "Search the web", detail: shorten(action.query || item.query || null), state };
396
+ }
397
+ return null;
398
+ }
399
+ function unwrapShell(command) {
400
+ const wrapped = /^(?:\S*\/)?(?:ba|z)?sh\s+-l?c\s+(['"])([\s\S]*)\1$/.exec(command.trim());
401
+ if (!wrapped) return command.trim();
402
+ return wrapped[1] === "'" ? wrapped[2].replaceAll(`'\\''`, `'`) : wrapped[2];
403
+ }
404
+ function connectorLabel(server, tool) {
405
+ const words = (name) => name.replace(/[_-]+/g, " ").trim();
406
+ if (server === "codex_apps") {
407
+ const said = words(tool);
408
+ return said.charAt(0).toUpperCase() + said.slice(1);
409
+ }
410
+ return `${words(server.replace(/^claude_ai_/, ""))}: ${words(tool)}`;
411
+ }
412
+ function compactArgs(input) {
413
+ if (input === null || input === void 0) return null;
414
+ if (typeof input === "object" && Object.keys(input).length === 0) return null;
415
+ return shorten(typeof input === "string" ? input : JSON.stringify(input));
416
+ }
417
+ function shorten(value) {
418
+ if (!value) return null;
419
+ const lines = value.trim().split("\n");
420
+ const first = lines[0].trim();
421
+ const more = lines.length > 1;
422
+ if (first.length > DETAIL_LENGTH) return `${first.slice(0, DETAIL_LENGTH - 1)}\u2026`;
423
+ return more ? `${first} \u2026` : first;
424
+ }
314
425
  function composePrompt(instructions, context, request, profile) {
315
426
  const now = /* @__PURE__ */ new Date();
316
427
  const facts = [`- Profile: ${profile}`, `- Today: ${now.toISOString().slice(0, 10)}`];
@@ -372,6 +483,7 @@ var Assistant = class _Assistant {
372
483
  sessionId: turn.sessionId,
373
484
  reason: turn.error,
374
485
  reply: turn.reply,
486
+ steps: turn.steps,
375
487
  at: turn.endedAt
376
488
  });
377
489
  }
@@ -431,6 +543,7 @@ ${typed}` : action.request : typed;
431
543
  endedAt: null,
432
544
  status: "running",
433
545
  reply: "",
546
+ steps: [],
434
547
  error: null
435
548
  };
436
549
  this.turns.set(turn.id, turn);
@@ -492,18 +605,27 @@ ${typed}` : action.request : typed;
492
605
  stderr.push(chunk.toString("utf8"));
493
606
  if (stderr.length > 50) stderr.shift();
494
607
  });
608
+ const calls = /* @__PURE__ */ new Map();
495
609
  if (child.stdout) {
496
610
  const lines = createInterface({ input: child.stdout });
497
611
  lines.on("line", (line) => {
498
612
  const event = adapter.parse(line);
499
613
  if (!event) return;
500
614
  if (event.sessionId) turn.sessionId = event.sessionId;
501
- if (event.text) {
502
- turn.reply = turn.reply ? `${turn.reply}
503
-
504
- ${event.text}` : event.text;
505
- this.hooks.onChange();
615
+ if (event.text) addStep(turn, { kind: "message", text: event.text, detail: null, state: "done" });
616
+ for (const parsed of event.steps ?? []) {
617
+ const known = calls.get(parsed.id);
618
+ if (known) {
619
+ if (parsed.text) known.text = parsed.text;
620
+ if (parsed.detail) known.detail = parsed.detail;
621
+ known.state = parsed.state;
622
+ } else if (parsed.text) {
623
+ const step = { kind: "tool", text: parsed.text, detail: parsed.detail ?? null, state: parsed.state };
624
+ calls.set(parsed.id, step);
625
+ addStep(turn, step);
626
+ }
506
627
  }
628
+ if (event.text || event.steps) this.hooks.onChange();
507
629
  if (event.done) done = event.done;
508
630
  });
509
631
  }
@@ -535,20 +657,24 @@ ${event.text}` : event.text;
535
657
  }
536
658
  async finish(turn, outcome, aborted = false) {
537
659
  const at = (/* @__PURE__ */ new Date()).toISOString();
538
- const reply = outcome.reply ?? turn.reply;
660
+ const last = turn.steps.findLast((step) => step.kind === "message");
661
+ const reply = outcome.ok ? outcome.reply ?? last?.text ?? "" : "";
662
+ if (outcome.ok && last && sameWords(last.text, reply)) turn.steps.splice(turn.steps.lastIndexOf(last), 1);
663
+ for (const step of turn.steps) if (step.state === "running") step.state = outcome.ok ? "done" : "failed";
539
664
  turn.reply = reply;
540
665
  turn.endedAt = at;
666
+ const steps = turn.steps;
541
667
  if (outcome.ok) {
542
668
  turn.status = "done";
543
- await this.append({ event: "finished", turn: turn.id, sessionId: turn.sessionId, reply, at });
669
+ await this.append({ event: "finished", turn: turn.id, sessionId: turn.sessionId, reply, steps, at });
544
670
  } else if (aborted) {
545
671
  turn.status = "aborted";
546
672
  turn.error = outcome.error ?? "stopped";
547
- await this.append({ event: "aborted", turn: turn.id, sessionId: turn.sessionId, reason: turn.error, reply, at });
673
+ await this.append({ event: "aborted", turn: turn.id, sessionId: turn.sessionId, reason: turn.error, reply, steps, at });
548
674
  } else {
549
675
  turn.status = "failed";
550
676
  turn.error = outcome.error ?? "failed";
551
- await this.append({ event: "failed", turn: turn.id, sessionId: turn.sessionId, error: turn.error, reply, at });
677
+ await this.append({ event: "failed", turn: turn.id, sessionId: turn.sessionId, error: turn.error, reply, steps, at });
552
678
  }
553
679
  this.hooks.onChange();
554
680
  }
@@ -440,15 +440,10 @@ function loadConfig(env = loadEnv()) {
440
440
  schemaFile: resolve2(dataDir, "items.schema.json"),
441
441
  schemaSource: resolve2(import.meta.dirname, "..", "schema", "items.schema.json"),
442
442
  archiveDir: resolve2(dataDir, "archive"),
443
- sessionFile: resolve2(dataDir, "session.json"),
444
- sessionsLogFile: resolve2(dataDir, "sessions.jsonl"),
445
443
  pullsFile: resolve2(dataDir, "prs.json"),
446
444
  calendarFile: resolve2(dataDir, "calendar.json"),
447
445
  ticketsFile: resolve2(dataDir, "tickets.json"),
448
446
  timeZoneFile: resolve2(dataDir, "timezone.json"),
449
- // The same 1 to 120 the session endpoint holds a start to.
450
- sessionMinutes: envBounded("DAILY_FOCUS_SESSION_MINUTES", 25, 1, 120, env),
451
- awayAfterMinutes: envBounded("DAILY_FOCUS_AWAY_AFTER", profile === "personal" ? 0 : 10, 0, 24 * 60, env),
452
447
  port: envBounded("DAILY_FOCUS_PORT", 4321, 0, 65535, env),
453
448
  host: envString("DAILY_FOCUS_HOST", "127.0.0.1", env),
454
449
  freeWindows: envFlag("DAILY_FOCUS_FREE_WINDOWS", profile !== "personal", env),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  localDateKey
3
- } from "./chunk-RNE27FQY.js";
3
+ } from "./chunk-KZNY45XW.js";
4
4
 
5
5
  // src/sample.ts
6
6
  function timeOn(now, hour, minute = 0) {