opencode-courier 0.3.0 → 0.4.1
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 +180 -274
- package/dist/index.js +47 -73
- package/dist/kernel/protocol.d.ts +20 -0
- package/dist/kernel/protocol.js +36 -0
- package/dist/kernel/tool.d.ts +5 -0
- package/dist/kernel/tool.js +5 -0
- package/dist/person/events.d.ts +23 -0
- package/dist/person/events.js +113 -0
- package/dist/person/form.d.ts +26 -0
- package/dist/person/form.js +64 -0
- package/dist/person/index.d.ts +17 -0
- package/dist/person/index.js +5 -0
- package/dist/{relay.d.ts → person/permission.d.ts} +27 -21
- package/dist/{relay.js → person/permission.js} +9 -5
- package/dist/person/ports.d.ts +58 -0
- package/dist/person/ports.js +2 -0
- package/dist/{question → person/question}/answer.d.ts +33 -4
- package/dist/{question → person/question}/answer.js +43 -8
- package/dist/person/question/index.d.ts +7 -0
- package/dist/{question → person/question}/index.js +3 -3
- package/dist/{question → person/question}/lifecycle.d.ts +1 -1
- package/dist/{question → person/question}/pure.d.ts +1 -2
- package/dist/{question → person/question}/pure.js +1 -1
- package/dist/{question → person/question}/relay.d.ts +1 -1
- package/dist/{question → person/question}/relay.js +14 -8
- package/dist/{question → person/question}/shared.d.ts +13 -3
- package/dist/person/question/shared.js +52 -0
- package/dist/person/question/types.d.ts +121 -0
- package/dist/person/question/types.js +1 -0
- package/dist/person/rpc-handlers.d.ts +160 -0
- package/dist/person/rpc-handlers.js +62 -0
- package/dist/person/rpc.d.ts +227 -0
- package/dist/person/rpc.js +70 -0
- package/dist/person/text.d.ts +68 -0
- package/dist/person/text.js +262 -0
- package/dist/person/tools.d.ts +20 -0
- package/dist/person/tools.js +55 -0
- package/dist/person/tui.d.ts +46 -0
- package/dist/{bounded.d.ts → platform/bounded.d.ts} +4 -0
- package/dist/{bounded.js → platform/bounded.js} +4 -0
- package/dist/platform/events.d.ts +34 -0
- package/dist/platform/events.js +121 -0
- package/dist/platform/hub.d.ts +195 -0
- package/dist/{hub.js → platform/hub.js} +27 -71
- package/dist/{version.js → platform/version.js} +1 -1
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.js +3 -0
- package/dist/scheduling/index.d.ts +6 -0
- package/dist/scheduling/index.js +5 -0
- package/dist/{later.d.ts → scheduling/later.d.ts} +7 -3
- package/dist/{later.js → scheduling/later.js} +12 -4
- package/dist/scheduling/text.d.ts +6 -0
- package/dist/scheduling/text.js +10 -0
- package/dist/scheduling/tools.d.ts +14 -0
- package/dist/scheduling/tools.js +48 -0
- package/dist/skill.d.ts +4 -0
- package/dist/skill.js +54 -1
- package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +1 -27
- package/dist/{cleanup.js → tree/cleanup.js} +9 -16
- package/dist/tree/context.d.ts +15 -0
- package/dist/tree/context.js +34 -0
- package/dist/tree/domain.d.ts +101 -0
- package/dist/tree/domain.js +68 -0
- package/dist/{watch.d.ts → tree/events.d.ts} +10 -37
- package/dist/tree/events.js +204 -0
- package/dist/tree/forget.d.ts +24 -0
- package/dist/tree/forget.js +59 -0
- package/dist/tree/group.d.ts +82 -0
- package/dist/tree/group.js +203 -0
- package/dist/tree/index.d.ts +10 -0
- package/dist/tree/index.js +11 -0
- package/dist/tree/limits.d.ts +10 -0
- package/dist/{limits.js → tree/limits.js} +6 -55
- package/dist/tree/panel.d.ts +13 -0
- package/dist/tree/panel.js +75 -0
- package/dist/tree/ports.d.ts +276 -0
- package/dist/tree/ports.js +1 -0
- package/dist/tree/report.d.ts +23 -0
- package/dist/tree/report.js +33 -0
- package/dist/tree/roster.d.ts +27 -0
- package/dist/tree/roster.js +62 -0
- package/dist/tree/rpc-handlers.d.ts +201 -0
- package/dist/tree/rpc-handlers.js +20 -0
- package/dist/tree/rpc.d.ts +344 -0
- package/dist/tree/rpc.js +83 -0
- package/dist/tree/send.d.ts +14 -0
- package/dist/tree/send.js +99 -0
- package/dist/tree/spawn.d.ts +21 -0
- package/dist/tree/spawn.js +173 -0
- package/dist/tree/status.d.ts +128 -0
- package/dist/tree/status.js +62 -0
- package/dist/{stop.d.ts → tree/stop.d.ts} +1 -9
- package/dist/{stop.js → tree/stop.js} +9 -13
- package/dist/tree/store.d.ts +24 -0
- package/dist/tree/store.js +264 -0
- package/dist/{tree.d.ts → tree/subtree.d.ts} +5 -6
- package/dist/{tree.js → tree/subtree.js} +7 -5
- package/dist/tree/summary.d.ts +7 -0
- package/dist/tree/summary.js +35 -0
- package/dist/tree/text.d.ts +77 -0
- package/dist/tree/text.js +287 -0
- package/dist/tree/tools.d.ts +54 -0
- package/dist/{tools.js → tree/tools.js} +12 -142
- package/dist/tree/tui.d.ts +53 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +1342 -0
- package/dist/webhook/index.d.ts +5 -0
- package/dist/webhook/index.js +3 -0
- package/dist/{webhook.d.ts → webhook/receiver.d.ts} +17 -23
- package/dist/{webhook.js → webhook/receiver.js} +58 -50
- package/dist/webhook/subscriptions.d.ts +29 -0
- package/dist/webhook/subscriptions.js +52 -0
- package/dist/webhook/text.d.ts +23 -0
- package/dist/webhook/text.js +82 -0
- package/dist/webhook/tools.d.ts +19 -0
- package/dist/webhook/tools.js +46 -0
- package/package.json +18 -7
- package/dist/courier.d.ts +0 -200
- package/dist/courier.js +0 -315
- package/dist/group.d.ts +0 -86
- package/dist/group.js +0 -247
- package/dist/hub.d.ts +0 -300
- package/dist/limits.d.ts +0 -65
- package/dist/notices.d.ts +0 -356
- package/dist/notices.js +0 -678
- package/dist/question/index.d.ts +0 -6
- package/dist/question/shared.js +0 -24
- package/dist/report.d.ts +0 -48
- package/dist/report.js +0 -88
- package/dist/roster.d.ts +0 -93
- package/dist/roster.js +0 -206
- package/dist/tools.d.ts +0 -94
- package/dist/watch.js +0 -474
- /package/dist/{question → person/question}/lifecycle.js +0 -0
- /package/dist/{question → person/question}/runtime.d.ts +0 -0
- /package/dist/{question → person/question}/runtime.js +0 -0
- /package/dist/{json.d.ts → platform/json.d.ts} +0 -0
- /package/dist/{json.js → platform/json.js} +0 -0
- /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
- /package/dist/{storage.js → platform/storage.js} +0 -0
- /package/dist/{version.d.ts → platform/version.d.ts} +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/** The tree's model-facing texts: the child brief, the role part, refusals, reports, notices and tool results. */
|
|
2
|
+
import { obj, str } from "../platform/json.js";
|
|
3
|
+
import { clipText, defuse, END_TURN, SKILL_ID, SPLIT_LINE, STATUSES } from "../kernel/protocol.js";
|
|
4
|
+
// The envelope and the child brief.
|
|
5
|
+
/** How a text names the statuses a report may carry. */
|
|
6
|
+
const STATUS_LIST = `${STATUSES.slice(0, -1).join(", ")} or ${STATUSES.at(-1)}`;
|
|
7
|
+
/** The branch an isolated child commits its work on, and names in the `branch` of its report. */
|
|
8
|
+
export const childBranch = (sessionID) => `courier/${sessionID}`;
|
|
9
|
+
/** What a spawned session at `depth` is told; `depth` 1 is a child of a session nobody spawned. `branch` is for an isolated one. */
|
|
10
|
+
export function childBrief(parentID, task, depth, limits, branch) {
|
|
11
|
+
return [
|
|
12
|
+
`You were started by session ${parentID} through opencode-courier.`,
|
|
13
|
+
"",
|
|
14
|
+
`When you finish, send exactly one report: call courier_send with sessionID "${parentID}", a short account of what you did and ` +
|
|
15
|
+
"status done, partial (say what is left and why) or failed (say what went wrong). Add artifacts where there are any: " +
|
|
16
|
+
"branch, commits, files, checks (each command you ran and its result).",
|
|
17
|
+
"If you need a decision from the session that started you, which you cannot make yourself, send status blocked with a " +
|
|
18
|
+
"concrete ask, then end your turn and wait for its answer; never end quietly.",
|
|
19
|
+
"A report with a status wakes the parent and is the only way it hears from you, so do not end without sending one. " +
|
|
20
|
+
"A message without a status is progress, not your report: if you end your turn without a report, the parent is told so.",
|
|
21
|
+
"If you need the person to decide something, use your question tool; it reaches them through the session that started you.",
|
|
22
|
+
"",
|
|
23
|
+
...(branch ? [...isolationRule(branch, depth < limits.maxDepth), ""] : []),
|
|
24
|
+
...splitRule(parentID, depth, limits),
|
|
25
|
+
"",
|
|
26
|
+
"Task:",
|
|
27
|
+
task,
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
function isolationRule(branch, mayPass) {
|
|
31
|
+
return [
|
|
32
|
+
"You work in a git worktree of your own, on a detached HEAD. Commit your work on the branch " +
|
|
33
|
+
`${branch}: create it first (git switch -c ${branch}), and name it in the branch of your report's artifacts, so the ` +
|
|
34
|
+
"session that started you can merge it. Uncommitted work is not in your report.",
|
|
35
|
+
...(mayPass
|
|
36
|
+
? [
|
|
37
|
+
`If you split your task and start a session with isolate: true, commit first: its worktree starts from your current HEAD, ` +
|
|
38
|
+
"not from your uncommitted changes. Merge each branch it reports into yours before you report.",
|
|
39
|
+
]
|
|
40
|
+
: []),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
function splitRule(parentID, depth, limits) {
|
|
44
|
+
if (depth >= limits.maxDepth)
|
|
45
|
+
return [`You are at depth ${depth}, the deepest the session tree goes, so you cannot start sessions: do the task yourself.`];
|
|
46
|
+
return [
|
|
47
|
+
`You are at depth ${depth} of a session tree that goes at most ${limits.maxDepth} deep. Split your task or do it yourself:`,
|
|
48
|
+
"- Split it when it has 2 or more independent parts, each substantial and touching separate files or areas: start one " +
|
|
49
|
+
`session per part with courier_spawn, at most ${limits.maxChildren} of yours running at once and ${limits.maxTotal} in the whole tree.`,
|
|
50
|
+
"- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session.",
|
|
51
|
+
`- Say which in one line of your reply: ${SPLIT_LINE}.`,
|
|
52
|
+
"- If you split, you orchestrate: end your turn while they work, check and integrate each part yourself when it " +
|
|
53
|
+
`reports (never hand that checking to another session), then send ${parentID} one combined report.`,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
/** How every part the context hook adds begins, so a second copy of the plugin does not add its own. */
|
|
57
|
+
export const ROLE_PREFIX = "opencode-courier role:";
|
|
58
|
+
/**
|
|
59
|
+
* The system part naming a session's place in its tree: `depth` 0 for a session nobody spawned that
|
|
60
|
+
* has started one, `null` for one that has started none, which is pointed to the skill. The same on every request.
|
|
61
|
+
*/
|
|
62
|
+
export function rolePart(depth, limits) {
|
|
63
|
+
const { maxDepth, maxChildren, maxTotal } = limits;
|
|
64
|
+
const budget = `At most ${maxChildren} of the sessions you start run at once, and ${maxTotal} in the whole tree.`;
|
|
65
|
+
if (depth === null)
|
|
66
|
+
return (`${ROLE_PREFIX} root, no sessions started yet. Before a task that touches several files or areas, load the ` +
|
|
67
|
+
`${SKILL_ID} skill and decide whether to split it across sessions (doing it yourself is fine), and say ` +
|
|
68
|
+
`which in one line of your reply: ${SPLIT_LINE}`);
|
|
69
|
+
if (depth === 0)
|
|
70
|
+
return (`${ROLE_PREFIX} root orchestrator. You started sessions with courier_spawn, and they may start their own, ` +
|
|
71
|
+
`down to ${maxDepth} levels below you. ${budget} Check and integrate what each one reports yourself.`);
|
|
72
|
+
if (depth < maxDepth)
|
|
73
|
+
return (`${ROLE_PREFIX} sub-orchestrator, at depth ${depth} of at most ${maxDepth}. Split your task with courier_spawn or ` +
|
|
74
|
+
`do it yourself, as your brief says; sessions can be started ${count(maxDepth - depth, "level")} below you. ${budget}`);
|
|
75
|
+
return (`${ROLE_PREFIX} leaf, at depth ${depth}, the deepest the session tree goes. You cannot start sessions: do your ` +
|
|
76
|
+
"task yourself and report with courier_send to the session that started you.");
|
|
77
|
+
}
|
|
78
|
+
// courier_spawn's refusals, one per limit.
|
|
79
|
+
export const depthRefusal = (depth, maxDepth) => `Not started: this session is at depth ${depth} of its session tree, and the tree goes at most ${maxDepth} deep ` +
|
|
80
|
+
"(maxDepth), so it cannot start sessions. Do the task yourself, and report with courier_send.";
|
|
81
|
+
/** How a refusal names sessions that count against a limit: those running, and those that have not reported yet. */
|
|
82
|
+
const live = (n) => (n === 1 ? "is running or has not reported yet" : "are running or have not reported yet");
|
|
83
|
+
export const childrenRefusal = (running, maxChildren) => `Not started: ${count(running, "session")} you started ${live(running)}, the most allowed at once ` +
|
|
84
|
+
`(maxChildren ${maxChildren}). Do this part yourself, or end your turn and start it once one of them has reported.`;
|
|
85
|
+
export const totalRefusal = (running, maxTotal) => `Not started: ${count(running, "session")} in this session tree ${live(running)}, the most allowed at once ` +
|
|
86
|
+
`(maxTotal ${maxTotal}). Do this part yourself, or end your turn and try again once a report has arrived.`;
|
|
87
|
+
/** The strings of a list given as artifacts, whatever else the model put in it. */
|
|
88
|
+
const strings = (value) => (Array.isArray(value) ? value.flatMap((item) => (typeof item === "string" ? [item.trim()] : [])) : []).filter(Boolean);
|
|
89
|
+
/** A list's line in the artifacts block, or none when it is empty. */
|
|
90
|
+
const listed = (name, items) => (items.length ? [`- ${name}: ${items.join(", ")}`] : []);
|
|
91
|
+
/**
|
|
92
|
+
* The body of a report: its text, then the artifacts in a fixed layout, the same for every report,
|
|
93
|
+
* so a parent finds the branch, the commits, the files and the checks without reading prose.
|
|
94
|
+
*/
|
|
95
|
+
export function reportBody(message, artifacts) {
|
|
96
|
+
const given = obj(artifacts);
|
|
97
|
+
const checks = (Array.isArray(given.checks) ? given.checks : []).flatMap((check) => {
|
|
98
|
+
const command = str(obj(check).command)?.trim();
|
|
99
|
+
return command ? [` - ${command}: ${str(obj(check).result)?.trim() || "(no result given)"}`] : [];
|
|
100
|
+
});
|
|
101
|
+
const lines = [
|
|
102
|
+
...listed("branch", strings([given.branch])),
|
|
103
|
+
...listed("commits", strings(given.commits)),
|
|
104
|
+
...listed("files", strings(given.files)),
|
|
105
|
+
...(checks.length ? ["- checks:", ...checks] : []),
|
|
106
|
+
];
|
|
107
|
+
return lines.length ? [message, "", "Artifacts:", ...lines].join("\n") : message;
|
|
108
|
+
}
|
|
109
|
+
// Tool results.
|
|
110
|
+
export function spawnText(child) {
|
|
111
|
+
const warning = (child.fromParent ? " Its worktree starts from the commit your own worktree is on." : "") +
|
|
112
|
+
(child.uncommitted
|
|
113
|
+
? " Your worktree has uncommitted changes, which it does not see: commit them and tell it, or send them to it, if it needs them."
|
|
114
|
+
: "") +
|
|
115
|
+
(child.rosterError ? ` It is not on your courier_children list: ${child.rosterError}` : "") +
|
|
116
|
+
(child.groupError ? ` It is in no group, so its report comes on its own: ${child.groupError}` : "");
|
|
117
|
+
const reports = child.group
|
|
118
|
+
? `Its report is held with group "${child.group}": once every session you started in that group has reported and your ` +
|
|
119
|
+
"turn has ended, you get their reports in one message, which starts a new turn for you; a blocked report, a question " +
|
|
120
|
+
"or a failed turn still reaches you at once."
|
|
121
|
+
: "It will report back with courier_send, which starts a new turn for you.";
|
|
122
|
+
return (`Started session ${child.sessionID} in ${child.directory}. ${reports} Once you have started every session you need, ` +
|
|
123
|
+
`end your turn: reply without calling more tools. That does not drop the task; you carry on with it when the reports arrive.${warning}`);
|
|
124
|
+
}
|
|
125
|
+
/** courier_send's result: for a message to the session that started the sender, whether it was its report. */
|
|
126
|
+
export function sendText(sessionID, sent = {}) {
|
|
127
|
+
const status = sent.status ? `, status ${sent.status}` : "";
|
|
128
|
+
if (sent.held) {
|
|
129
|
+
const { group, reported, members } = sent.held;
|
|
130
|
+
const counted = reported !== undefined && members !== undefined;
|
|
131
|
+
let when = `once ${sessionID}'s turn has ended, yours being the last.`;
|
|
132
|
+
if (!counted || reported < members) {
|
|
133
|
+
const have = counted ? ` (${reported} of ${members} have)` : "";
|
|
134
|
+
when = `once every member of the group has reported${have}. Nothing more is needed from you for it.`;
|
|
135
|
+
}
|
|
136
|
+
return `Recorded as your report${status}. It is held with group "${group}" and goes to ${sessionID} in one message with the group's other reports, ${when} ${END_TURN}`;
|
|
137
|
+
}
|
|
138
|
+
if (sent.report === true)
|
|
139
|
+
return `Delivered to ${sessionID} as your report${status}. ${END_TURN}`;
|
|
140
|
+
if (sent.report === false)
|
|
141
|
+
return `Delivered to ${sessionID}. Without a status it is progress, not your report: when you finish, send one with status ${STATUS_LIST}.`;
|
|
142
|
+
return `Delivered to ${sessionID}.`;
|
|
143
|
+
}
|
|
144
|
+
/** courier_status's result: the session's state, as JSON. */
|
|
145
|
+
export const statusText = (status) => JSON.stringify(status, null, 2);
|
|
146
|
+
export const childrenText = (listed) => listed.length ? JSON.stringify(listed, null, 2) : "No sessions started with courier_spawn.";
|
|
147
|
+
export function cleanupText(result) {
|
|
148
|
+
if (result.outcome === "kept")
|
|
149
|
+
return `Kept the worktree ${result.directory} of ${result.sessionID}: it has ${result.reason}. Commit or branch what you want to keep, or call courier_cleanup again with force: true to discard it.`;
|
|
150
|
+
const done = {
|
|
151
|
+
removed: `Removed the worktree ${result.directory} of ${result.sessionID}.`,
|
|
152
|
+
gone: `The worktree ${result.directory} of ${result.sessionID} was already gone; dropped it from courier_children.`,
|
|
153
|
+
forgotten: `${result.sessionID} ran in ${result.directory}, with no worktree of its own to remove; dropped it from courier_children.`,
|
|
154
|
+
}[result.outcome];
|
|
155
|
+
if (!result.group)
|
|
156
|
+
return done;
|
|
157
|
+
return `${done} Dropped it from group "${result.group}" without a report: the group's other reports reach you once they are in and your turn has ended, the message naming it, and a report it sends after all comes on its own.`;
|
|
158
|
+
}
|
|
159
|
+
/** courier_tree's result: the sessions under a session, drawn depth first, as JSON. */
|
|
160
|
+
export function treeText(tree) {
|
|
161
|
+
if (!tree.sessions.length)
|
|
162
|
+
return "No sessions started with courier_spawn.";
|
|
163
|
+
const note = tree.truncated === undefined ? "" : `\nOnly the first ${tree.truncated} sessions are listed (maxTotal); courier_tree on one of them lists what is under it.`;
|
|
164
|
+
return JSON.stringify(tree, null, 2) + note;
|
|
165
|
+
}
|
|
166
|
+
/** What courier_stop did to each session, and to the messages and worktrees. */
|
|
167
|
+
export function stopText(result) {
|
|
168
|
+
const interrupted = result.stopped.filter((one) => one.outcome === "interrupted").length;
|
|
169
|
+
const failed = result.stopped.filter((one) => one.outcome === "failed");
|
|
170
|
+
return [
|
|
171
|
+
`Stopped ${result.sessionID} and ${count(result.stopped.length - 1, "session")} under it, deepest first: ${interrupted} had a turn running and was interrupted, ` +
|
|
172
|
+
`${result.stopped.filter((one) => one.outcome === "idle" || one.outcome === "gone").length} did not run.`,
|
|
173
|
+
...result.stopped.map((one) => `- ${one.sessionID}: ${one.outcome}${one.error ? " (" + one.error + ")" : ""}`),
|
|
174
|
+
...(failed.length ? ["Those that failed may still run: try courier_stop again."] : []),
|
|
175
|
+
result.cancelled.length
|
|
176
|
+
? `Cancelled ${count(result.cancelled.length, "scheduled message")}: ${result.cancelled.join(", ")}.`
|
|
177
|
+
: "No scheduled messages were pending for them.",
|
|
178
|
+
...(result.told ? [`Told ${result.told}, which started ${result.sessionID}, that it was stopped.`] : []),
|
|
179
|
+
...(result.cleanup ? worktreeLines(result.cleanup) : []),
|
|
180
|
+
].join("\n");
|
|
181
|
+
}
|
|
182
|
+
const FORCE_HINT = " Commit or branch what you want to keep, or call courier_cleanup again with force: true to discard it.";
|
|
183
|
+
/** courier_stop's lines on the worktrees it was asked to remove. */
|
|
184
|
+
function worktreeLines(cleanup) {
|
|
185
|
+
if (!cleanup.length)
|
|
186
|
+
return ["None of them ran in a worktree of its own."];
|
|
187
|
+
return cleanup.map((one) => one.outcome === "failed"
|
|
188
|
+
? `Worktree of ${one.sessionID}: not removed (${one.error}).`
|
|
189
|
+
: cleanupText(one).replace(FORCE_HINT, " Kept; courier_cleanup with force: true discards it."));
|
|
190
|
+
}
|
|
191
|
+
/** What the parent of a stopped session is told when the stop was ordered by a session above both. */
|
|
192
|
+
export const stoppedNotice = (title, by) => [
|
|
193
|
+
`This session, "${title}", which you started with courier_spawn, was stopped by ${by}, a session above you, together with the sessions under it.`,
|
|
194
|
+
"It was not a failure and it will not report back. Carry on without it, or start a replacement if you still need what it was doing.",
|
|
195
|
+
].join("\n");
|
|
196
|
+
/** Why a worktree in this state must be kept, or undefined when removing it loses nothing. */
|
|
197
|
+
export function keepReason(state) {
|
|
198
|
+
const reasons = [
|
|
199
|
+
state.changes.length ? `${count(state.changes.length, "uncommitted change")} (${preview(state.changes)})` : "",
|
|
200
|
+
state.commits.length
|
|
201
|
+
? `${count(state.commits.length, "commit")} on no branch, tag or remote (${preview(state.commits)})`
|
|
202
|
+
: "",
|
|
203
|
+
].filter(Boolean);
|
|
204
|
+
return reasons.length ? reasons.join(" and ") : undefined;
|
|
205
|
+
}
|
|
206
|
+
function count(n, noun) {
|
|
207
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
208
|
+
}
|
|
209
|
+
function preview(items) {
|
|
210
|
+
return items.length > 5 ? `${items.slice(0, 5).join(", ")}, ...` : items.join(", ");
|
|
211
|
+
}
|
|
212
|
+
// Turn failures.
|
|
213
|
+
export function failureNotice(title, error, group) {
|
|
214
|
+
const status = error.status === undefined ? "" : `, status ${error.status}`;
|
|
215
|
+
return [
|
|
216
|
+
`This session, "${title}", which you started with courier_spawn, failed: ${error.message} (${error.type}${status}).`,
|
|
217
|
+
"Its turn ended without finishing, so it will not report back on its own.",
|
|
218
|
+
"Message it with courier_send to have it try again, start a replacement, or carry on without it.",
|
|
219
|
+
...(group
|
|
220
|
+
? [
|
|
221
|
+
`It is a member of group "${group}", which no longer waits for it: its other members' reports reach you once they have ` +
|
|
222
|
+
"reported and your turn has ended, and if it reports after all before then, its report joins them.",
|
|
223
|
+
]
|
|
224
|
+
: []),
|
|
225
|
+
].join("\n");
|
|
226
|
+
}
|
|
227
|
+
/** Longest last reply a notice that a session ended its turn without reporting quotes. */
|
|
228
|
+
const MAX_REPLY = 2000;
|
|
229
|
+
/**
|
|
230
|
+
* What the parent is told when a session it started ends its turn without a report to it;
|
|
231
|
+
* `progressed` when it messaged the parent without a status since its last prompt.
|
|
232
|
+
*/
|
|
233
|
+
export function silentNotice(title, lastText, progressed = false, group) {
|
|
234
|
+
const reply = lastText?.trim();
|
|
235
|
+
return [
|
|
236
|
+
`This session, "${title}", which you started with courier_spawn, ended its turn without reporting back with courier_send, and does nothing more on its own.`,
|
|
237
|
+
...(progressed
|
|
238
|
+
? [`Its last message to you had no status, so it was progress, not its report: a report carries status ${STATUS_LIST}.`]
|
|
239
|
+
: []),
|
|
240
|
+
...(reply ? ["Its last reply:", defuse(clipText(reply, MAX_REPLY))] : ["It ended without a reply."]),
|
|
241
|
+
"",
|
|
242
|
+
"Decide what it needs: message it with courier_send to have it carry on, or to report if its last reply is what you needed; or start a replacement.",
|
|
243
|
+
"Until it reports, it counts toward your limits on the sessions you run at once.",
|
|
244
|
+
...(group
|
|
245
|
+
? [
|
|
246
|
+
`It is a member of group "${group}", whose other reports are held until it reports: have it report, as above, so the group ` +
|
|
247
|
+
"reaches you, or, if you no longer need its report, drop it with courier_cleanup, which releases the group without it.",
|
|
248
|
+
]
|
|
249
|
+
: []),
|
|
250
|
+
].join("\n");
|
|
251
|
+
}
|
|
252
|
+
// Join groups.
|
|
253
|
+
/** How a release names each status among its reports: `2 done, 1 failed`. */
|
|
254
|
+
function statusCounts(reports) {
|
|
255
|
+
return STATUSES.flatMap((status) => {
|
|
256
|
+
const n = reports.filter((report) => report.status === status).length;
|
|
257
|
+
return n ? [`${n} ${status}`] : [];
|
|
258
|
+
}).join(", ");
|
|
259
|
+
}
|
|
260
|
+
const LEFT_HOW = {
|
|
261
|
+
failed: "its turn failed",
|
|
262
|
+
interrupted: "its turn was interrupted",
|
|
263
|
+
deleted: "it was deleted",
|
|
264
|
+
dropped: "you dropped it with courier_cleanup",
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* What the parent gets when a group it named is released: every report, with its status, text and artifacts in
|
|
268
|
+
* `reportBody`'s layout, and the members that left without one, in the order they joined.
|
|
269
|
+
*/
|
|
270
|
+
export function groupNotice(group, reports, left = []) {
|
|
271
|
+
const total = reports.length + left.length;
|
|
272
|
+
const listed = reports.flatMap((report, index) => [
|
|
273
|
+
"",
|
|
274
|
+
`[${index + 1}/${reports.length}] ${report.sessionID} "${report.title}": ${report.status}`,
|
|
275
|
+
reportBody(report.message, report.artifacts),
|
|
276
|
+
]);
|
|
277
|
+
const named = left.map((member) => `${member.sessionID} "${member.title}" (${LEFT_HOW[member.by]})`).join(", ");
|
|
278
|
+
const without = left.length ? ["", `Without a report: ${named}.`] : [];
|
|
279
|
+
const missing = left.length ? `, ${count(left.length, "member")} without a report, named at the end` : "";
|
|
280
|
+
return [
|
|
281
|
+
`Group "${group}" is complete: ${count(reports.length, "report")} of ${count(total, "member")} (${statusCounts(reports)})${missing}. Each report follows, with its status.`,
|
|
282
|
+
...listed,
|
|
283
|
+
...without,
|
|
284
|
+
"",
|
|
285
|
+
"Check and integrate each one yourself. The sessions that reported are idle; message one with courier_send if it has more to do.",
|
|
286
|
+
].join("\n");
|
|
287
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { type ToolEditor } from "../kernel/tool.js";
|
|
3
|
+
import type { CleanupPorts, CourierPorts, StopPorts } from "./ports.js";
|
|
4
|
+
/**
|
|
5
|
+
* The tree's courier tools: their input schemas, descriptions, and what each returns. The descriptions are
|
|
6
|
+
* model-facing like the texts in `text.ts`, and snapshotted with them in `test/notices.test.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SpawnInput: Schema.Struct<{
|
|
9
|
+
readonly task: Schema.String;
|
|
10
|
+
readonly title: Schema.optional<Schema.String>;
|
|
11
|
+
readonly agent: Schema.optional<Schema.String>;
|
|
12
|
+
readonly isolate: Schema.optional<Schema.Boolean>;
|
|
13
|
+
readonly group: Schema.optional<Schema.String>;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const SendInput: Schema.Struct<{
|
|
16
|
+
readonly sessionID: Schema.String;
|
|
17
|
+
readonly message: Schema.String;
|
|
18
|
+
readonly status: Schema.optional<Schema.Literals<readonly ["done", "partial", "blocked", "failed"]>>;
|
|
19
|
+
readonly artifacts: Schema.optional<Schema.Struct<{
|
|
20
|
+
readonly branch: Schema.optional<Schema.String>;
|
|
21
|
+
readonly commits: Schema.optional<Schema.$Array<Schema.String>>;
|
|
22
|
+
readonly files: Schema.optional<Schema.$Array<Schema.String>>;
|
|
23
|
+
readonly checks: Schema.optional<Schema.$Array<Schema.Struct<{
|
|
24
|
+
readonly command: Schema.String;
|
|
25
|
+
readonly result: Schema.String;
|
|
26
|
+
}>>>;
|
|
27
|
+
}>>;
|
|
28
|
+
readonly queue: Schema.optional<Schema.Boolean>;
|
|
29
|
+
}>;
|
|
30
|
+
export declare const StatusInput: Schema.Struct<{
|
|
31
|
+
readonly sessionID: Schema.String;
|
|
32
|
+
}>;
|
|
33
|
+
export declare const ChildrenInput: Schema.Struct<{
|
|
34
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
35
|
+
}>;
|
|
36
|
+
export declare const TreeInput: Schema.Struct<{
|
|
37
|
+
readonly sessionID: Schema.optional<Schema.String>;
|
|
38
|
+
}>;
|
|
39
|
+
export declare const StopInput: Schema.Struct<{
|
|
40
|
+
readonly sessionID: Schema.String;
|
|
41
|
+
readonly cleanup: Schema.optional<Schema.Boolean>;
|
|
42
|
+
}>;
|
|
43
|
+
export declare const CleanupInput: Schema.Struct<{
|
|
44
|
+
readonly sessionID: Schema.String;
|
|
45
|
+
readonly force: Schema.optional<Schema.Boolean>;
|
|
46
|
+
}>;
|
|
47
|
+
/** What the tree's tools run on: one plugin instance's ports. */
|
|
48
|
+
export interface TreeToolPorts {
|
|
49
|
+
readonly courier: CourierPorts;
|
|
50
|
+
readonly cleanup: CleanupPorts;
|
|
51
|
+
readonly stop: StopPorts;
|
|
52
|
+
}
|
|
53
|
+
/** Adds the tree's courier tools, each a direct tool rather than a code-mode one. */
|
|
54
|
+
export declare function addTreeTools(tools: ToolEditor, ports: TreeToolPorts): void;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { STATUSES } from "../kernel/protocol.js";
|
|
3
|
+
import { rethrow } from "../kernel/tool.js";
|
|
2
4
|
import { cleanup } from "./cleanup.js";
|
|
5
|
+
import { send } from "./send.js";
|
|
6
|
+
import { spawn } from "./spawn.js";
|
|
7
|
+
import { listChildren, status } from "./status.js";
|
|
3
8
|
import { stop } from "./stop.js";
|
|
4
|
-
import { tree } from "./
|
|
5
|
-
import {
|
|
6
|
-
import { cancel, schedule } from "./later.js";
|
|
7
|
-
import { answerText, cancelText, childrenText, cleanupText, laterText, REPLIES, sendText, spawnText, STATUSES, statusText, stopText, subscribeText, treeText, unsubscribeText, } from "./notices.js";
|
|
8
|
-
import { answerQuestion, isQuestion } from "./question/index.js";
|
|
9
|
-
import { answer } from "./relay.js";
|
|
10
|
-
import { subscribe, unsubscribe } from "./webhook.js";
|
|
9
|
+
import { tree } from "./subtree.js";
|
|
10
|
+
import { childrenText, cleanupText, sendText, spawnText, statusText, stopText, treeText } from "./text.js";
|
|
11
11
|
/**
|
|
12
|
-
* The courier tools: their input schemas, descriptions, and what each returns. The descriptions are
|
|
13
|
-
* model-facing like the texts in `
|
|
12
|
+
* The tree's courier tools: their input schemas, descriptions, and what each returns. The descriptions are
|
|
13
|
+
* model-facing like the texts in `text.ts`, and snapshotted with them in `test/notices.test.ts`.
|
|
14
14
|
*/
|
|
15
15
|
export const SpawnInput = Schema.Struct({
|
|
16
16
|
task: Schema.String.annotate({ description: "What the new session should do. It is told who started it and how to report back." }),
|
|
@@ -67,69 +67,8 @@ export const CleanupInput = Schema.Struct({
|
|
|
67
67
|
description: "Remove it even with uncommitted changes or commits on no branch; that work is lost.",
|
|
68
68
|
})),
|
|
69
69
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
delayMinutes: Schema.optional(
|
|
73
|
-
// Some models send the number as a string ("3", "3.0") whatever the schema says, and resend it
|
|
74
|
-
// unchanged after the error, so a string is accepted and schedule reads it as a number.
|
|
75
|
-
Schema.Union([Schema.Number, Schema.String]).annotate({
|
|
76
|
-
description: "Deliver this many minutes from now. Give this or at.",
|
|
77
|
-
})),
|
|
78
|
-
at: Schema.optional(Schema.String.annotate({ description: "Deliver at this ISO 8601 time. Give this or delayMinutes." })),
|
|
79
|
-
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to deliver to; defaults to this one." })),
|
|
80
|
-
});
|
|
81
|
-
export const AnswerInput = Schema.Struct({
|
|
82
|
-
sessionID: Schema.String.annotate({ description: "The session you started that is waiting, as its notice names it." }),
|
|
83
|
-
requestID: Schema.String.annotate({ description: "The request id from the notice." }),
|
|
84
|
-
reply: Schema.optional(Schema.Literals(REPLIES).annotate({
|
|
85
|
-
description: "For a permission request: the choice the person made, once, always (only when the notice offers it) or reject.",
|
|
86
|
-
})),
|
|
87
|
-
message: Schema.optional(Schema.String.annotate({ description: "For a permission request, with reject: the person's reason, passed on with the refusal." })),
|
|
88
|
-
answers: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Array(Schema.String)])).annotate({
|
|
89
|
-
description: "For a question: one entry per question, in order, each the label the person chose or the text they gave, " +
|
|
90
|
-
"or a list of labels where the question allows several.",
|
|
91
|
-
})),
|
|
92
|
-
});
|
|
93
|
-
export const CancelInput = Schema.Struct({
|
|
94
|
-
id: Schema.String.annotate({ description: "The id courier_later returned." }),
|
|
95
|
-
});
|
|
96
|
-
export const SubscribeInput = Schema.Struct({
|
|
97
|
-
topic: Schema.String.annotate({
|
|
98
|
-
description: "owner/repo for every event of a GitHub repository, owner/repo#<number> for one pull request or issue " +
|
|
99
|
-
"(reviews, comments, completed CI runs), or a plain name for deliveries posted to /hook/<name>.",
|
|
100
|
-
}),
|
|
101
|
-
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to subscribe; defaults to this one." })),
|
|
102
|
-
});
|
|
103
|
-
export const UnsubscribeInput = Schema.Struct({
|
|
104
|
-
topic: Schema.optional(Schema.String.annotate({ description: "The topic to drop; all of this session's when omitted." })),
|
|
105
|
-
sessionID: Schema.optional(Schema.String.annotate({ description: "The session to unsubscribe; defaults to this one." })),
|
|
106
|
-
});
|
|
107
|
-
const rethrow = (tool) => (error) => {
|
|
108
|
-
throw describeFailure(tool, error);
|
|
109
|
-
};
|
|
110
|
-
// One tool answers both: a question's id is the plugin's own, a permission request's OpenCode's.
|
|
111
|
-
async function answerRequest(ports, callerID, input) {
|
|
112
|
-
const { sessionID, requestID } = input;
|
|
113
|
-
if (isQuestion(requestID)) {
|
|
114
|
-
if (input.reply !== undefined || input.message !== undefined)
|
|
115
|
-
throw new Error(`${requestID} is a question: pass the person's answers in answers, not reply or message.`);
|
|
116
|
-
if (input.answers === undefined)
|
|
117
|
-
throw new Error(`${requestID} is a question: answers is required, one entry per question.`);
|
|
118
|
-
return answerQuestion(ports.questions, callerID, { sessionID, requestID, answers: input.answers });
|
|
119
|
-
}
|
|
120
|
-
if (input.answers !== undefined)
|
|
121
|
-
throw new Error(`${requestID} is a permission request: pass the person's choice in reply (once, always or reject), not answers.`);
|
|
122
|
-
if (input.reply === undefined)
|
|
123
|
-
throw new Error(`${requestID} is a permission request: reply is required, once, always or reject.`);
|
|
124
|
-
return answer(ports.answer, ports.waiting, callerID, {
|
|
125
|
-
sessionID,
|
|
126
|
-
requestID,
|
|
127
|
-
reply: input.reply,
|
|
128
|
-
...(input.message !== undefined ? { message: input.message } : {}),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
/** Adds the courier tools, each a direct tool rather than a code-mode one. */
|
|
132
|
-
export function addTools(tools, ports) {
|
|
70
|
+
/** Adds the tree's courier tools, each a direct tool rather than a code-mode one. */
|
|
71
|
+
export function addTreeTools(tools, ports) {
|
|
133
72
|
tools.add({
|
|
134
73
|
name: "courier_spawn",
|
|
135
74
|
options: { codemode: false },
|
|
@@ -218,80 +157,11 @@ export function addTools(tools, ports) {
|
|
|
218
157
|
"the result lists them; commit or branch what you want, or pass force: true to discard it. A session without " +
|
|
219
158
|
"isolate has no worktree and is only dropped from courier_children. Either way, one in a join group that has not " +
|
|
220
159
|
"reported is dropped from it, so the group's other reports reach you without it; use this for a member you no " +
|
|
221
|
-
"longer need a report from.",
|
|
160
|
+
"longer need a report from. It never deletes the session itself, nor the sessions it started.",
|
|
222
161
|
input: CleanupInput,
|
|
223
162
|
execute: async (input, context) => {
|
|
224
163
|
const result = await cleanup(ports.cleanup, context.sessionID, input).catch(rethrow("courier_cleanup"));
|
|
225
164
|
return { content: cleanupText(result), metadata: result };
|
|
226
165
|
},
|
|
227
166
|
});
|
|
228
|
-
tools.add({
|
|
229
|
-
name: "courier_answer",
|
|
230
|
-
options: { codemode: false },
|
|
231
|
-
description: "Pass on the answer to a permission request or a question that a session you started with courier_spawn (or " +
|
|
232
|
-
"one started from it) waits on, after a notice from it named the request. The answer is the person's, not yours: " +
|
|
233
|
-
"first ask the person you are working with, offering the choices the notice lists, then call this with what " +
|
|
234
|
-
"they chose: reply for a permission request, answers for a question. Never choose for them. The session " +
|
|
235
|
-
"carries on once it has the answer.",
|
|
236
|
-
input: AnswerInput,
|
|
237
|
-
execute: async (input, context) => {
|
|
238
|
-
const result = await answerRequest(ports, context.sessionID, input).catch(rethrow("courier_answer"));
|
|
239
|
-
return { content: answerText(result), metadata: result };
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
tools.add({
|
|
243
|
-
name: "courier_later",
|
|
244
|
-
options: { codemode: false },
|
|
245
|
-
description: "Schedule a message for a session (this one by default), delivered when due and waking it if idle. " +
|
|
246
|
-
"Use it as a safety net when you start sessions: schedule one check-in, end your turn by replying without " +
|
|
247
|
-
"calling more tools, and cancel it with courier_cancel if the child reports first. Survives server restarts; " +
|
|
248
|
-
"may arrive up to ~15 seconds late.",
|
|
249
|
-
input: LaterInput,
|
|
250
|
-
execute: async (input, context) => {
|
|
251
|
-
const entry = await schedule(ports.later, context.sessionID, input).catch(rethrow("courier_later"));
|
|
252
|
-
const fireAt = new Date(entry.fireAt).toISOString();
|
|
253
|
-
return {
|
|
254
|
-
content: laterText(entry, fireAt, entry.sessionID === context.sessionID),
|
|
255
|
-
metadata: { id: entry.id, fireAt, sessionID: entry.sessionID },
|
|
256
|
-
};
|
|
257
|
-
},
|
|
258
|
-
});
|
|
259
|
-
tools.add({
|
|
260
|
-
name: "courier_cancel",
|
|
261
|
-
options: { codemode: false },
|
|
262
|
-
description: "Cancel a message scheduled with courier_later, e.g. because the child it was waiting for reported.",
|
|
263
|
-
input: CancelInput,
|
|
264
|
-
execute: async (input) => {
|
|
265
|
-
const cancelled = await cancel(ports.later, input.id).catch(rethrow("courier_cancel"));
|
|
266
|
-
return { content: cancelText(input.id, cancelled), metadata: { id: input.id, cancelled } };
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
tools.add({
|
|
270
|
-
name: "courier_subscribe",
|
|
271
|
-
options: { codemode: false },
|
|
272
|
-
description: "Wake a session (this one by default) when a webhook arrives for a topic: a GitHub repository, one of its " +
|
|
273
|
-
"pull requests or issues (reviews, comments, completed CI runs), or a named generic hook. Each matching " +
|
|
274
|
-
"delivery arrives as a message, queued behind any running turn. End your turn and wait; do not poll.",
|
|
275
|
-
input: SubscribeInput,
|
|
276
|
-
execute: async (input, context) => {
|
|
277
|
-
const sessionID = input.sessionID ?? context.sessionID;
|
|
278
|
-
const subscription = await subscribe(ports.hooks, sessionID, input.topic).catch(rethrow("courier_subscribe"));
|
|
279
|
-
const receiving = await ports.receiving();
|
|
280
|
-
return {
|
|
281
|
-
content: subscribeText(sessionID, subscription.topic, receiving),
|
|
282
|
-
metadata: { sessionID, topic: subscription.topic, receiver: receiving },
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
tools.add({
|
|
287
|
-
name: "courier_unsubscribe",
|
|
288
|
-
options: { codemode: false },
|
|
289
|
-
description: "Stop webhook deliveries for a topic, or all of a session's topics, e.g. once its pull request is merged.",
|
|
290
|
-
input: UnsubscribeInput,
|
|
291
|
-
execute: async (input, context) => {
|
|
292
|
-
const sessionID = input.sessionID ?? context.sessionID;
|
|
293
|
-
const dropped = await unsubscribe(ports.hooks, sessionID, input.topic).catch(rethrow("courier_unsubscribe"));
|
|
294
|
-
return { content: unsubscribeText(sessionID, dropped), metadata: { sessionID, dropped } };
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
167
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/tui";
|
|
2
|
+
import { type Row, type Stopped, type Summary, type Wait } from "./rpc.js";
|
|
3
|
+
type Context = Plugin.Context;
|
|
4
|
+
/** How long after the first of a burst of changes the entry asks the server again. */
|
|
5
|
+
export declare const SETTLE_MS = 250;
|
|
6
|
+
/** The panel's settle: each of its asks reads every row's messages, so a busy tree is asked at most once a second. */
|
|
7
|
+
export declare const PANEL_SETTLE_MS = 1000;
|
|
8
|
+
/** The name of the tree panel in the `session.panel` slot. */
|
|
9
|
+
export declare const PANEL = "courier-tree";
|
|
10
|
+
/** How often the panel redraws its running times and how long ago each last tool call was. */
|
|
11
|
+
export declare const TICK_MS = 1000;
|
|
12
|
+
/** The status entry's text, such as "courier 2 running · 1 waiting on you · group pair 1/2"; empty when there is nothing to show. */
|
|
13
|
+
export declare function summaryLine(summary: Summary): string;
|
|
14
|
+
/** A span of time, coarsest first: "45s", "3m 05s", "2h 07m". */
|
|
15
|
+
export declare function duration(ms: number): string;
|
|
16
|
+
/** How many of a session's children have reported, out of those it started, among the rows read. */
|
|
17
|
+
export declare function parts(rows: ReadonlyArray<Row>, parentID: string): {
|
|
18
|
+
reported: number;
|
|
19
|
+
started: number;
|
|
20
|
+
};
|
|
21
|
+
/** A bar of parts reported, `width` cells wide: "[████░░░░░░] 2/5 reported". */
|
|
22
|
+
export declare function bar({ reported, started }: {
|
|
23
|
+
reported: number;
|
|
24
|
+
started: number;
|
|
25
|
+
}, width?: number): string;
|
|
26
|
+
/** What a paused session waits on, such as "asks permission: shell echo hi". */
|
|
27
|
+
export declare function waitLine(wait: Wait): string;
|
|
28
|
+
/** What a row's session is doing, its report and its group standing: "idle · reported done · group pair held". */
|
|
29
|
+
export declare function statusLine(row: Row): string;
|
|
30
|
+
/** A leaf's activity: how long it has run, its last tool call and how long ago, and its cost. */
|
|
31
|
+
export declare function activityLine(row: Row, now: number): string;
|
|
32
|
+
/** The lines a row shows under its title: an orchestrator's bar, a paused session's requests, or else a live leaf's activity. */
|
|
33
|
+
export declare function detailLines(row: Row, rows: ReadonlyArray<Row>, now: number): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Adds the tree's entry to the prompt's footer: what the sessions under the open session do, asked of the RPC after each
|
|
36
|
+
* change; nothing on the home screen. Clicked while some wait on the person, it calls `onWaiting` with the open session.
|
|
37
|
+
*/
|
|
38
|
+
export declare function addTreeStatus(ctx: Context, onWaiting?: (sessionID: string) => void): () => void;
|
|
39
|
+
/** The toast after a stop: how many sessions were interrupted, and how many scheduled messages were cancelled. */
|
|
40
|
+
export declare function stoppedLine({ stopped, cancelled }: Stopped): string;
|
|
41
|
+
/** The usage the `/courier` command shows for an input it does not know. */
|
|
42
|
+
export declare const USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
|
|
43
|
+
/**
|
|
44
|
+
* Adds the tree panel to the `session.panel` slot, opened by `/courier tree` or the palette, which follows the person
|
|
45
|
+
* between sessions while open, and `/courier stop <session>`, which stops a session under the open one once confirmed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function addTreePanel(ctx: Context): () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Draws the person's attention when reports arrive: a join group's reports, released to the session that started them,
|
|
50
|
+
* and a child's report to its parent, with the `subagent_done` sound. Read from the courier's own delivery metadata.
|
|
51
|
+
*/
|
|
52
|
+
export declare function addTreeAttention(ctx: Context): () => void;
|
|
53
|
+
export {};
|
package/dist/tui.d.ts
ADDED