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 +6 -1
- package/SETUP.md +10 -2
- package/dist/{audit-F26VGPOO.js → audit-JVDLHD3B.js} +3 -3
- package/dist/{chunk-EV44BC2F.js → chunk-6COKG3VL.js} +19 -4
- package/dist/{chunk-CPJ6LOVC.js → chunk-D3JAWEF7.js} +1 -1
- package/dist/{chunk-4DZEBUPU.js → chunk-DPMHC72S.js} +3 -1
- package/dist/{chunk-GVLNDQVQ.js → chunk-HEP46XRX.js} +2 -2
- package/dist/{chunk-HVCCIZGA.js → chunk-TUZHZ3NP.js} +1 -1
- package/dist/cli.js +6 -6
- package/dist/{demo-UMGG3VKY.js → demo-AJWHY6KY.js} +72 -5
- package/dist/{init-MFPXASX3.js → init-BZSCAS3L.js} +1 -1
- package/dist/{seed-S32JX5MW.js → seed-WQEXDK2L.js} +3 -3
- package/dist/{server-EDPYEZCP.js → server-VPSSCDBW.js} +321 -30
- package/dist/{service-XPFMYIHR.js → service-RRHZ7BJN.js} +1 -1
- package/package.json +1 -1
- package/prompts/README.md +14 -0
- package/prompts/morning-brief-personal.md +37 -0
- package/prompts/morning-brief-work.md +37 -0
- package/public/app.js +89 -1
- package/public/style.css +58 -0
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,
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
+
} from "./chunk-D3JAWEF7.js";
|
|
5
5
|
import {
|
|
6
6
|
laterISO,
|
|
7
7
|
localDateKey,
|
|
8
8
|
parseISO
|
|
9
|
-
} from "./chunk-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
@@ -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")
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
173
|
+
await import("./init-BZSCAS3L.js");
|
|
174
174
|
return;
|
|
175
175
|
case "audit":
|
|
176
|
-
await import("./audit-
|
|
176
|
+
await import("./audit-JVDLHD3B.js");
|
|
177
177
|
return;
|
|
178
178
|
case "seed":
|
|
179
|
-
await import("./seed-
|
|
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-
|
|
6
|
+
} from "./chunk-6COKG3VL.js";
|
|
7
7
|
import {
|
|
8
8
|
dateFromNow,
|
|
9
9
|
sampleBrief,
|
|
10
10
|
sampleGeneratedAt
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-TUZHZ3NP.js";
|
|
12
|
+
import "./chunk-D3JAWEF7.js";
|
|
13
13
|
import {
|
|
14
14
|
PROFILES,
|
|
15
15
|
localDateKey
|
|
16
|
-
} from "./chunk-
|
|
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) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sampleBrief
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TUZHZ3NP.js";
|
|
4
4
|
import {
|
|
5
5
|
Store
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-D3JAWEF7.js";
|
|
7
7
|
import {
|
|
8
8
|
loadConfig
|
|
9
|
-
} from "./chunk-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
3202
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
4672
|
-
const schemaFile =
|
|
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 >
|
|
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 =
|
|
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(
|
|
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))
|
package/package.json
CHANGED
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;
|