opencode-courier 0.13.8 → 0.14.0
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 +7 -9
- package/dist/index.d.ts +0 -8
- package/dist/index.js +6 -53
- package/dist/kernel/protocol.d.ts +0 -14
- package/dist/kernel/protocol.js +1 -15
- package/dist/kernel/tool.d.ts +0 -2
- package/dist/kernel/tool.js +0 -1
- package/dist/memory/domain.d.ts +6 -85
- package/dist/memory/domain.js +7 -63
- package/dist/memory/events.d.ts +0 -11
- package/dist/memory/events.js +1 -12
- package/dist/memory/forget.d.ts +0 -5
- package/dist/memory/forget.js +0 -4
- package/dist/memory/index.js +0 -2
- package/dist/memory/lessons.d.ts +0 -8
- package/dist/memory/lessons.js +0 -9
- package/dist/memory/note.d.ts +0 -9
- package/dist/memory/note.js +0 -5
- package/dist/memory/pin.d.ts +0 -5
- package/dist/memory/pin.js +0 -4
- package/dist/memory/ports.d.ts +2 -22
- package/dist/memory/recall.d.ts +0 -5
- package/dist/memory/recall.js +0 -12
- package/dist/memory/record.d.ts +0 -4
- package/dist/memory/record.js +8 -31
- package/dist/memory/rpc-handlers.d.ts +3 -6
- package/dist/memory/rpc-handlers.js +0 -6
- package/dist/memory/rpc.d.ts +6 -19
- package/dist/memory/rpc.js +1 -16
- package/dist/memory/store.d.ts +0 -5
- package/dist/memory/store.js +1 -16
- package/dist/memory/sweep.d.ts +0 -4
- package/dist/memory/sweep.js +0 -4
- package/dist/memory/text.d.ts +0 -25
- package/dist/memory/text.js +6 -36
- package/dist/memory/tools.d.ts +0 -2
- package/dist/memory/tools.js +0 -2
- package/dist/memory/tui.d.ts +1 -35
- package/dist/person/events.d.ts +0 -16
- package/dist/person/events.js +3 -30
- package/dist/person/form.d.ts +0 -9
- package/dist/person/form.js +0 -10
- package/dist/person/index.d.ts +0 -2
- package/dist/person/permission.d.ts +0 -34
- package/dist/person/permission.js +2 -28
- package/dist/person/ports.d.ts +0 -16
- package/dist/person/ports.js +0 -1
- package/dist/person/question/answer.d.ts +0 -28
- package/dist/person/question/answer.js +0 -49
- package/dist/person/question/index.js +0 -2
- package/dist/person/question/lifecycle.d.ts +0 -26
- package/dist/person/question/lifecycle.js +1 -32
- package/dist/person/question/pure.d.ts +0 -11
- package/dist/person/question/pure.js +0 -11
- package/dist/person/question/relay.d.ts +0 -1
- package/dist/person/question/relay.js +5 -67
- package/dist/person/question/runtime.d.ts +0 -14
- package/dist/person/question/runtime.js +1 -26
- package/dist/person/question/shared.d.ts +0 -17
- package/dist/person/question/shared.js +0 -20
- package/dist/person/question/types.d.ts +0 -44
- package/dist/person/rpc-handlers.d.ts +0 -7
- package/dist/person/rpc-handlers.js +0 -6
- package/dist/person/rpc.d.ts +0 -20
- package/dist/person/rpc.js +0 -16
- package/dist/person/text.d.ts +0 -45
- package/dist/person/text.js +2 -54
- package/dist/person/tools.d.ts +0 -3
- package/dist/person/tools.js +1 -3
- package/dist/person/tui.d.ts +0 -21
- package/dist/platform/bounded.d.ts +0 -8
- package/dist/platform/bounded.js +0 -8
- package/dist/platform/events.d.ts +0 -18
- package/dist/platform/events.js +3 -22
- package/dist/platform/hub.d.ts +0 -113
- package/dist/platform/hub.js +1 -76
- package/dist/platform/json.d.ts +0 -3
- package/dist/platform/json.js +1 -3
- package/dist/platform/storage.d.ts +0 -7
- package/dist/platform/storage.js +0 -6
- package/dist/platform/version.d.ts +0 -9
- package/dist/platform/version.js +0 -8
- package/dist/rpc.js +0 -1
- package/dist/scheduling/index.d.ts +0 -1
- package/dist/scheduling/index.js +0 -1
- package/dist/scheduling/later.d.ts +0 -11
- package/dist/scheduling/later.js +0 -14
- package/dist/scheduling/text.d.ts +0 -1
- package/dist/scheduling/text.js +0 -1
- package/dist/scheduling/tools.d.ts +0 -1
- package/dist/scheduling/tools.js +1 -5
- package/dist/skill.d.ts +1 -9
- package/dist/skill.js +4 -12
- package/dist/tree/cleanup.d.ts +0 -20
- package/dist/tree/cleanup.js +0 -26
- package/dist/tree/context.d.ts +0 -5
- package/dist/tree/context.js +0 -8
- package/dist/tree/domain.d.ts +0 -64
- package/dist/tree/domain.js +7 -45
- package/dist/tree/events.d.ts +0 -36
- package/dist/tree/events.js +4 -49
- package/dist/tree/forget.d.ts +0 -15
- package/dist/tree/forget.js +0 -18
- package/dist/tree/group.d.ts +0 -22
- package/dist/tree/group.js +1 -37
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.js +1 -3
- package/dist/tree/limits.d.ts +0 -4
- package/dist/tree/limits.js +1 -12
- package/dist/tree/mascot.d.ts +0 -25
- package/dist/tree/mascot.js +1 -32
- package/dist/tree/model.d.ts +0 -5
- package/dist/tree/model.js +2 -15
- package/dist/tree/nudge.d.ts +0 -11
- package/dist/tree/nudge.js +3 -16
- package/dist/tree/panel.d.ts +0 -8
- package/dist/tree/panel.js +3 -16
- package/dist/tree/ports.d.ts +2 -138
- package/dist/tree/report.d.ts +0 -10
- package/dist/tree/report.js +0 -11
- package/dist/tree/roster.d.ts +0 -21
- package/dist/tree/roster.js +0 -21
- package/dist/tree/rpc-handlers.d.ts +6 -1
- package/dist/tree/rpc-handlers.js +0 -5
- package/dist/tree/rpc.d.ts +9 -109
- package/dist/tree/rpc.js +2 -38
- package/dist/tree/running.d.ts +0 -6
- package/dist/tree/running.js +2 -10
- package/dist/tree/send.d.ts +0 -4
- package/dist/tree/send.js +4 -31
- package/dist/tree/spawn.d.ts +0 -7
- package/dist/tree/spawn.js +6 -47
- package/dist/tree/standing.d.ts +0 -8
- package/dist/tree/standing.js +1 -11
- package/dist/tree/status.d.ts +0 -8
- package/dist/tree/status.js +1 -9
- package/dist/tree/stop.d.ts +2 -9
- package/dist/tree/stop.js +58 -25
- package/dist/tree/store.d.ts +0 -8
- package/dist/tree/store.js +4 -38
- package/dist/tree/subtree.d.ts +8 -26
- package/dist/tree/subtree.js +37 -35
- package/dist/tree/summary.d.ts +0 -4
- package/dist/tree/summary.js +0 -8
- package/dist/tree/text.d.ts +1 -44
- package/dist/tree/text.js +5 -65
- package/dist/tree/tools.d.ts +0 -6
- package/dist/tree/tools.js +4 -8
- package/dist/tree/tui.d.ts +0 -81
- package/dist/tree/wake.d.ts +0 -30
- package/dist/tree/wake.js +5 -39
- package/dist/tui.js +22 -8
- package/package.json +8 -9
- package/dist/webhook/index.d.ts +0 -5
- package/dist/webhook/index.js +0 -3
- package/dist/webhook/receiver.d.ts +0 -80
- package/dist/webhook/receiver.js +0 -339
- package/dist/webhook/subscriptions.d.ts +0 -29
- package/dist/webhook/subscriptions.js +0 -52
- package/dist/webhook/text.d.ts +0 -23
- package/dist/webhook/text.js +0 -82
- package/dist/webhook/tools.d.ts +0 -19
- package/dist/webhook/tools.js +0 -46
package/dist/person/rpc.js
CHANGED
|
@@ -1,55 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The person's RPC, the contract its TUI part, the asks inbox, and its server half share. Standard Schema values,
|
|
3
|
-
* as the tree's; imports only `@opencode/plugin/rpc` and `effect`.
|
|
4
|
-
*/
|
|
5
1
|
import { Rpc } from "@opencode/plugin/rpc";
|
|
6
2
|
import { Schema } from "effect";
|
|
7
3
|
const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
8
|
-
/** A session of the tree, as the inbox names where an ask comes from. */
|
|
9
4
|
const Session = Schema.Struct({
|
|
10
5
|
sessionID: Schema.String,
|
|
11
6
|
title: Schema.String,
|
|
12
7
|
parentID: Schema.String,
|
|
13
|
-
/** A child of the tree's top session is at depth 1. */
|
|
14
8
|
depth: Count,
|
|
15
9
|
group: Schema.optionalKey(Schema.String),
|
|
16
10
|
});
|
|
17
|
-
/** A permission request a session of the tree, the top among them, waits on. */
|
|
18
11
|
const Permission = Schema.Struct({
|
|
19
12
|
sessionID: Schema.String,
|
|
20
13
|
requestID: Schema.String,
|
|
21
14
|
action: Schema.String,
|
|
22
15
|
resources: Schema.Array(Schema.String),
|
|
23
|
-
/** What "always" would allow from now on; absent when it is not offered. */
|
|
24
16
|
save: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
25
|
-
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
26
17
|
passesOn: Schema.optionalKey(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String })),
|
|
27
18
|
});
|
|
28
|
-
/** One question of OpenCode's question tool. */
|
|
29
19
|
const Prompt = Schema.Struct({
|
|
30
20
|
question: Schema.String,
|
|
31
21
|
header: Schema.String,
|
|
32
22
|
options: Schema.Array(Schema.Struct({ label: Schema.String, description: Schema.String })),
|
|
33
23
|
multiple: Schema.optionalKey(Schema.Boolean),
|
|
34
24
|
});
|
|
35
|
-
/** A question a session of the tree waits on; `stopped` when its call was cut off and the answer goes as a message. */
|
|
36
25
|
const Question = Schema.Struct({
|
|
37
26
|
sessionID: Schema.String,
|
|
38
27
|
requestID: Schema.String,
|
|
39
28
|
questions: Schema.Array(Prompt),
|
|
40
29
|
stopped: Schema.optionalKey(Schema.Boolean),
|
|
41
30
|
});
|
|
42
|
-
/** What the sessions of a courier tree wait on the person for, but forms, which the TUI lists itself. */
|
|
43
31
|
const Asks = Schema.Struct({
|
|
44
|
-
/** The session at the top of the tree, which the sessions below were started from. */
|
|
45
32
|
top: Schema.String,
|
|
46
|
-
/** The sessions below the top, in the order a tree is drawn. */
|
|
47
33
|
sessions: Schema.Array(Session),
|
|
48
|
-
/** The top session's own requests first, then those of the sessions below. */
|
|
49
34
|
permissions: Schema.Array(Permission),
|
|
50
35
|
questions: Schema.Array(Question),
|
|
51
36
|
});
|
|
52
|
-
/** What answering a question did; `problem` says why nothing was passed on, when it was refused. */
|
|
53
37
|
const Answered = Schema.Struct({
|
|
54
38
|
answered: Schema.Boolean,
|
|
55
39
|
by: Schema.optionalKey(Schema.Literals(["result", "message"])),
|
package/dist/person/text.d.ts
CHANGED
|
@@ -1,73 +1,28 @@
|
|
|
1
1
|
import type { Asked, Elsewhere, Form, Outcome, PermissionAnswered, PermissionRequest, Prompt, QuestionAnswered, Reply, Withdrawal } from "./ports.js";
|
|
2
|
-
/**
|
|
3
|
-
* What courier_answer did: a permission request's result carries the reply, a question's whether its
|
|
4
|
-
* answers went out as the result of its call or, since that call had been cut off, as a message.
|
|
5
|
-
*/
|
|
6
2
|
export type AnswerResult = PermissionAnswered | QuestionAnswered;
|
|
7
3
|
export declare function answerText(result: AnswerResult): string;
|
|
8
|
-
/**
|
|
9
|
-
* Sent with a rejection that has no reason of its own. OpenCode ends the child's turn on a bare
|
|
10
|
-
* rejection, and the child would never report back; with a message, the call fails and it carries on.
|
|
11
|
-
*/
|
|
12
4
|
export declare const REJECTED = "Refused by the session that started you. Do without it, or report back why you cannot.";
|
|
13
|
-
/**
|
|
14
|
-
* The failure of a courier_answer whose proposed once OpenCode refused in the caller's session: a rule
|
|
15
|
-
* denies the action there, or the person rejected the prompt with a reason, which the child was given.
|
|
16
|
-
*/
|
|
17
5
|
export declare function refusedText(sessionID: string, requestID: string, reason: string): string;
|
|
18
|
-
/** How a notice names where the asking session came from, to the session at the top of its lineage. */
|
|
19
6
|
export declare const origin: (startedBy?: string) => string;
|
|
20
|
-
/** For a notice that a session's turn ended without it finishing. */
|
|
21
7
|
export declare const STAYS_QUIET = "and then it does not report back on its own: if it stays quiet, message it with courier_send to have it carry on.";
|
|
22
|
-
/**
|
|
23
|
-
* What the session at the top is told. `startedBy` names the session that started the asking one
|
|
24
|
-
* when that is not the top session itself, but one started from it.
|
|
25
|
-
*/
|
|
26
8
|
export declare function permissionNotice(title: string, request: PermissionRequest, startedBy?: string, native?: boolean): string;
|
|
27
|
-
/** What the session told about a permission request is told once it is answered without courier_answer. */
|
|
28
9
|
export declare function permissionSettledNotice(title: string, requestID: string, reply: Reply): string;
|
|
29
|
-
/** The `metadata.kind` of the forms OpenCode's web search asks its provider with. */
|
|
30
10
|
export declare const WEBSEARCH_FORM = "websearch.provider";
|
|
31
|
-
/** A form's `metadata.kind`, when it has one that can go in a notice's envelope. */
|
|
32
11
|
export declare const kindOf: (form: Form) => string | undefined;
|
|
33
|
-
/**
|
|
34
|
-
* What the session at the top is told about a form OpenCode shows in a session started with
|
|
35
|
-
* courier_spawn, which the plugin cannot pass on or answer: only the person can, where OpenCode shows it.
|
|
36
|
-
*/
|
|
37
12
|
export declare function formNotice(title: string, form: Form, startedBy?: string, shownIn?: string, top?: string): string;
|
|
38
|
-
/** What the session told about a form is told once it is answered or withdrawn. */
|
|
39
13
|
export declare function formSettledNotice(title: string, formID: string, settled: "answered" | "cancelled"): string;
|
|
40
|
-
/** What OpenCode's question tool tells the asking model once it has the answers. */
|
|
41
14
|
export declare function answeredText(questions: ReadonlyArray<Prompt>, answers: ReadonlyArray<ReadonlyArray<string>>): string;
|
|
42
15
|
declare const CUT_OFF: {
|
|
43
16
|
stopped: string;
|
|
44
17
|
restarted: string;
|
|
45
18
|
};
|
|
46
|
-
/** Why a question's call was cut off: its turn was stopped, or OpenCode restarted or closed its location. */
|
|
47
19
|
export type CutOff = keyof typeof CUT_OFF;
|
|
48
|
-
/** What the top session is told about a question, when it is asked or after its call was cut off. */
|
|
49
20
|
export declare function questionNotice(asked: Asked, cutOff?: CutOff): string;
|
|
50
|
-
/**
|
|
51
|
-
* What the top session is told about a native child's question, which OpenCode shows the person in the
|
|
52
|
-
* top session's own view: they answer it there, so the top session does not ask them again.
|
|
53
|
-
*/
|
|
54
21
|
export declare function nativeQuestionNotice(asked: Asked): string;
|
|
55
|
-
/** What the top session is told about a question settled without it. */
|
|
56
22
|
export declare function questionSettledNotice(asked: Asked, how: Elsewhere): string;
|
|
57
|
-
/** The result of a spawned session's question call that the person dismissed through the top session. */
|
|
58
23
|
export declare const DISMISSED = "The person dismissed this question without answering it. Carry on without the answers, or report back with courier_send to the session that started you why you cannot.";
|
|
59
|
-
/** The message a spawned session whose question call was cut off gets, with the top session's outcome. */
|
|
60
24
|
export declare function cutOffAnswer(asked: Asked, outcome: Outcome): string;
|
|
61
|
-
/**
|
|
62
|
-
* What a top session's question call that no waiting question was linked to adds to its result, when
|
|
63
|
-
* questions with the same choices wait: `waiting` lists them.
|
|
64
|
-
*/
|
|
65
25
|
export declare function unlinkedNote(waiting: ReadonlyArray<Pick<Asked, "sessionID" | "requestID">>): string;
|
|
66
|
-
/** The result of a linked call withdrawn because its question was settled without it. */
|
|
67
26
|
export declare function withdrawnText(linked: Asked, how: Withdrawal): string;
|
|
68
|
-
/**
|
|
69
|
-
* What a linked call adds to the person's answers about passing them on: `passed` is "result",
|
|
70
|
-
* "message", undefined when it no longer waited, or the error as `error: <message>`.
|
|
71
|
-
*/
|
|
72
27
|
export declare function passedNote(linked: Asked, passed: string | undefined): string;
|
|
73
28
|
export {};
|
package/dist/person/text.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
/** The permission, form and question notices and the result of courier_answer, snapshotted with the other texts. */
|
|
2
1
|
import { END_TURN } from "../kernel/protocol.js";
|
|
3
2
|
export function answerText(result) {
|
|
4
|
-
//
|
|
5
|
-
// stored where every server on the data directory finds it.
|
|
3
|
+
// a permission request is pending only in the server running the turn
|
|
6
4
|
if (!result.answered && "reply" in result)
|
|
7
5
|
return (`Nothing was passed on: no request ${result.requestID} of ${result.sessionID} is pending in this OpenCode server. ` +
|
|
8
6
|
"It was answered some other way, or the session stopped waiting, or it waits in another OpenCode server on the " +
|
|
@@ -19,30 +17,16 @@ export function answerText(result) {
|
|
|
19
17
|
const how = result.by === "message" ? " as a message, since its question had been cut off" : "";
|
|
20
18
|
return `Passed the answers to question ${result.requestID} on to ${result.sessionID}${how}; it carries on and reports back with courier_send. ${END_TURN}`;
|
|
21
19
|
}
|
|
22
|
-
//
|
|
23
|
-
/**
|
|
24
|
-
* Sent with a rejection that has no reason of its own. OpenCode ends the child's turn on a bare
|
|
25
|
-
* rejection, and the child would never report back; with a message, the call fails and it carries on.
|
|
26
|
-
*/
|
|
20
|
+
// a bare rejection ends the child's turn (api-notes)
|
|
27
21
|
export const REJECTED = "Refused by the session that started you. Do without it, or report back why you cannot.";
|
|
28
|
-
/**
|
|
29
|
-
* The failure of a courier_answer whose proposed once OpenCode refused in the caller's session: a rule
|
|
30
|
-
* denies the action there, or the person rejected the prompt with a reason, which the child was given.
|
|
31
|
-
*/
|
|
32
22
|
export function refusedText(sessionID, requestID, reason) {
|
|
33
23
|
return `not allowed in your session (${reason}). The refusal was passed on with that reason to request ${requestID} of ${sessionID}, so its call fails and it carries on; it reports back with courier_send. Tell the person, and do not propose it again unless they ask.`;
|
|
34
24
|
}
|
|
35
25
|
const MAX_RESOURCES = 20;
|
|
36
26
|
const MAX_RESOURCE_LENGTH = 300;
|
|
37
27
|
const clip = (text) => (text.length > MAX_RESOURCE_LENGTH ? `${text.slice(0, MAX_RESOURCE_LENGTH - 3)}...` : text);
|
|
38
|
-
/** How a notice names where the asking session came from, to the session at the top of its lineage. */
|
|
39
28
|
export const origin = (startedBy) => startedBy ? `which ${startedBy} started with courier_spawn, a session started from yours,` : "which you started with courier_spawn,";
|
|
40
|
-
/** For a notice that a session's turn ended without it finishing. */
|
|
41
29
|
export const STAYS_QUIET = "and then it does not report back on its own: if it stays quiet, message it with courier_send to have it carry on.";
|
|
42
|
-
/**
|
|
43
|
-
* What the session at the top is told. `startedBy` names the session that started the asking one
|
|
44
|
-
* when that is not the top session itself, but one started from it.
|
|
45
|
-
*/
|
|
46
30
|
export function permissionNotice(title, request, startedBy, native = false) {
|
|
47
31
|
const resources = request.resources.slice(0, MAX_RESOURCES).map((resource) => `- ${clip(resource)}`);
|
|
48
32
|
if (request.resources.length > MAX_RESOURCES)
|
|
@@ -63,13 +47,11 @@ const permissionAsk = (request) => [
|
|
|
63
47
|
`Call courier_answer with sessionID "${request.sessionID}", requestID "${request.id}", reply set to the choice and, with reject, message set to the reason.`,
|
|
64
48
|
"Propose once unless the person has told you to refuse it, or the request plainly goes against what they asked of you; with once, the call returns when they have answered the prompt (or your rules allow the action without one). A refusal there with a reason fails the call with it, which the session is given; one without a reason ends your turn, and the session waits on.",
|
|
65
49
|
];
|
|
66
|
-
/** For a native child's request, which OpenCode shows the person itself, in the top session's view: nothing to call. */
|
|
67
50
|
const nativePermission = (request) => [
|
|
68
51
|
"OpenCode already shows this request to the person you are working with, as its own permission prompt in your session (and in the paired app's request dock), where they answer it; you will be told once it is answered.",
|
|
69
52
|
"Do not ask them yourself, with your question tool or in your reply, and do not call courier_answer for it, which would only ask them a second time: at most tell them in one line that this session waits on their permission, then end your turn.",
|
|
70
53
|
`Only if they tell you to answer it for them, call courier_answer with sessionID "${request.sessionID}", requestID "${request.id}" and reply set to once or reject (with reject, message set to the reason); with once, OpenCode asks them again in your session before it is passed on.`,
|
|
71
54
|
];
|
|
72
|
-
/** What the session told about a permission request is told once it is answered without courier_answer. */
|
|
73
55
|
export function permissionSettledNotice(title, requestID, reply) {
|
|
74
56
|
return [
|
|
75
57
|
`The permission request ${requestID} of this session, "${title}", has been answered (${reply}) without courier_answer, so it no longer waits on you.`,
|
|
@@ -79,10 +61,7 @@ export function permissionSettledNotice(title, requestID, reply) {
|
|
|
79
61
|
: []),
|
|
80
62
|
].join("\n");
|
|
81
63
|
}
|
|
82
|
-
// Forms.
|
|
83
|
-
/** The `metadata.kind` of the forms OpenCode's web search asks its provider with. */
|
|
84
64
|
export const WEBSEARCH_FORM = "websearch.provider";
|
|
85
|
-
/** A form's `metadata.kind`, when it has one that can go in a notice's envelope. */
|
|
86
65
|
export const kindOf = (form) => {
|
|
87
66
|
const kind = form.metadata?.kind;
|
|
88
67
|
return typeof kind === "string" && /^[\w.-]{1,60}$/.test(kind) ? kind : undefined;
|
|
@@ -100,7 +79,6 @@ function fieldLines(field) {
|
|
|
100
79
|
const when = field.when?.length ? " (only for some earlier answers)" : "";
|
|
101
80
|
return [`- ${clip(label)}${kind}${when}`, ...options];
|
|
102
81
|
}
|
|
103
|
-
/** The fields the person sees, as notice lines. */
|
|
104
82
|
function formLines(fields) {
|
|
105
83
|
const shown = fields.filter((field) => !field.hidden);
|
|
106
84
|
const lines = shown.slice(0, MAX_RESOURCES).flatMap(fieldLines);
|
|
@@ -114,10 +92,6 @@ const WEBSEARCH_NOTE = [
|
|
|
114
92
|
"The choice is kept for every session: once it is made, in any session, no session is asked again.",
|
|
115
93
|
'Besides answering the form, the person can make it by running a web search in their own session, or under OpenCode\'s "Third-party search" setting.',
|
|
116
94
|
];
|
|
117
|
-
/**
|
|
118
|
-
* Where the person answers a form: in its own session, or, for a native child, in the view of `shownIn`, the
|
|
119
|
-
* top of its native tree, where OpenCode lists the asks of the sessions under it; `top` is the session told.
|
|
120
|
-
*/
|
|
121
95
|
function formPlace(sessionID, shownIn, top) {
|
|
122
96
|
if (shownIn === sessionID)
|
|
123
97
|
return { where: `in session ${sessionID} itself`, which: "that session" };
|
|
@@ -126,10 +100,6 @@ function formPlace(sessionID, shownIn, top) {
|
|
|
126
100
|
return { where: `in your own session, ${lists}`, which: "your session" };
|
|
127
101
|
return { where: `in session ${shownIn}, ${lists}`, which: `session ${shownIn}` };
|
|
128
102
|
}
|
|
129
|
-
/**
|
|
130
|
-
* What the session at the top is told about a form OpenCode shows in a session started with
|
|
131
|
-
* courier_spawn, which the plugin cannot pass on or answer: only the person can, where OpenCode shows it.
|
|
132
|
-
*/
|
|
133
103
|
export function formNotice(title, form, startedBy, shownIn = form.sessionID, top = "") {
|
|
134
104
|
const { where, which } = formPlace(form.sessionID, shownIn, top);
|
|
135
105
|
return [
|
|
@@ -141,7 +111,6 @@ export function formNotice(title, form, startedBy, shownIn = form.sessionID, top
|
|
|
141
111
|
`Do not choose for them. Tell them ${which} shows this form and waits on them, then carry on.`,
|
|
142
112
|
].join("\n");
|
|
143
113
|
}
|
|
144
|
-
/** What the session told about a form is told once it is answered or withdrawn. */
|
|
145
114
|
export function formSettledNotice(title, formID, settled) {
|
|
146
115
|
return [
|
|
147
116
|
settled === "answered"
|
|
@@ -150,8 +119,6 @@ export function formSettledNotice(title, formID, settled) {
|
|
|
150
119
|
"If you told someone about it, tell them it is settled; there is nothing to pass on.",
|
|
151
120
|
].join("\n");
|
|
152
121
|
}
|
|
153
|
-
// Questions.
|
|
154
|
-
/** What OpenCode's question tool tells the asking model once it has the answers. */
|
|
155
122
|
export function answeredText(questions, answers) {
|
|
156
123
|
const formatted = questions
|
|
157
124
|
.map((prompt, index) => `"${prompt.question}"="${answers[index]?.length ? answers[index].join(", ") : "Unanswered"}"`)
|
|
@@ -167,7 +134,6 @@ const CUT_OFF = {
|
|
|
167
134
|
stopped: "its turn was stopped (interrupted, or ended by OpenCode after an hour without activity)",
|
|
168
135
|
restarted: "OpenCode restarted, or closed the session's project",
|
|
169
136
|
};
|
|
170
|
-
/** What the top session is told about a question, when it is asked or after its call was cut off. */
|
|
171
137
|
export function questionNotice(asked, cutOff) {
|
|
172
138
|
return [
|
|
173
139
|
cutOff
|
|
@@ -180,10 +146,6 @@ export function questionNotice(asked, cutOff) {
|
|
|
180
146
|
`What they choose there is passed on to the session${cutOff ? " as a message, which wakes it" : ""}. If you cannot use your question tool, or they answer some other way, call courier_answer with sessionID "${asked.sessionID}", requestID "${asked.requestID}" and answers: one entry per question, in order, each the label they chose or the text they gave (a list of labels where a question allows several).`,
|
|
181
147
|
].join("\n");
|
|
182
148
|
}
|
|
183
|
-
/**
|
|
184
|
-
* What the top session is told about a native child's question, which OpenCode shows the person in the
|
|
185
|
-
* top session's own view: they answer it there, so the top session does not ask them again.
|
|
186
|
-
*/
|
|
187
149
|
export function nativeQuestionNotice(asked) {
|
|
188
150
|
return [
|
|
189
151
|
`This session, "${asked.title}", ${origin(asked.startedBy)} asks the question below and waits for the answer.`,
|
|
@@ -194,7 +156,6 @@ export function nativeQuestionNotice(asked) {
|
|
|
194
156
|
`If they give you the answer some other way, call courier_answer with sessionID "${asked.sessionID}", requestID "${asked.requestID}" and answers: one entry per question, in order, each the label they chose or the text they gave (a list of labels where a question allows several).`,
|
|
195
157
|
].join("\n");
|
|
196
158
|
}
|
|
197
|
-
/** How a question was settled without the top session, as its settled notice puts it. */
|
|
198
159
|
function settledHow(asked, how) {
|
|
199
160
|
const where = asked.native || (how.by === "child" && how.via === "inbox") ? "by the person" : "in its own session";
|
|
200
161
|
if (how.by === "child")
|
|
@@ -203,7 +164,6 @@ function settledHow(asked, how) {
|
|
|
203
164
|
return `dismissed ${where}, without an answer`;
|
|
204
165
|
return `ended without an answer: its question call failed (${how.error})`;
|
|
205
166
|
}
|
|
206
|
-
/** What the top session is told about a question settled without it. */
|
|
207
167
|
export function questionSettledNotice(asked, how) {
|
|
208
168
|
const what = settledHow(asked, how);
|
|
209
169
|
return [
|
|
@@ -214,23 +174,16 @@ export function questionSettledNotice(asked, how) {
|
|
|
214
174
|
: [`That ends the session's turn, ${STAYS_QUIET}`]),
|
|
215
175
|
].join("\n");
|
|
216
176
|
}
|
|
217
|
-
/** The result of a spawned session's question call that the person dismissed through the top session. */
|
|
218
177
|
export const DISMISSED = "The person dismissed this question without answering it. Carry on without the answers, or report back with courier_send to the session that started you why you cannot.";
|
|
219
|
-
/** The message a spawned session whose question call was cut off gets, with the top session's outcome. */
|
|
220
178
|
export function cutOffAnswer(asked, outcome) {
|
|
221
179
|
return "answers" in outcome
|
|
222
180
|
? `Your question ${asked.requestID} was cut off before it was answered; here is the answer. ${answeredText(asked.questions, outcome.answers)}`
|
|
223
181
|
: `Your question ${asked.requestID} was cut off before it was answered, and then dismissed without an answer. Carry on without the answers, or report back with courier_send why you cannot.`;
|
|
224
182
|
}
|
|
225
|
-
/**
|
|
226
|
-
* What a top session's question call that no waiting question was linked to adds to its result, when
|
|
227
|
-
* questions with the same choices wait: `waiting` lists them.
|
|
228
|
-
*/
|
|
229
183
|
export function unlinkedNote(waiting) {
|
|
230
184
|
const which = waiting.map((question) => `session ${question.sessionID} (requestID "${question.requestID}")`).join(", ");
|
|
231
185
|
return `If you asked this for ${which}, these answers were not passed on by themselves: pass them on with courier_answer.`;
|
|
232
186
|
}
|
|
233
|
-
/** How a linked call's question was settled without it, as its withdrawn result puts it. */
|
|
234
187
|
function withdrawnHow(linked, how) {
|
|
235
188
|
switch (how.by) {
|
|
236
189
|
case "top":
|
|
@@ -245,14 +198,9 @@ function withdrawnHow(linked, how) {
|
|
|
245
198
|
return `ended: its question call failed (${how.error})`;
|
|
246
199
|
}
|
|
247
200
|
}
|
|
248
|
-
/** The result of a linked call withdrawn because its question was settled without it. */
|
|
249
201
|
export function withdrawnText(linked, how) {
|
|
250
202
|
return `Session ${linked.sessionID} no longer waits on this question: it was ${withdrawnHow(linked, how)}. There is nothing to pass on; tell the person it is settled.`;
|
|
251
203
|
}
|
|
252
|
-
/**
|
|
253
|
-
* What a linked call adds to the person's answers about passing them on: `passed` is "result",
|
|
254
|
-
* "message", undefined when it no longer waited, or the error as `error: <message>`.
|
|
255
|
-
*/
|
|
256
204
|
export function passedNote(linked, passed) {
|
|
257
205
|
if (passed === "result")
|
|
258
206
|
return `These answers were passed on to session ${linked.sessionID}, which carries on with them; do not call courier_answer for ${linked.requestID}. ${END_TURN}`;
|
package/dist/person/tools.d.ts
CHANGED
|
@@ -9,12 +9,9 @@ export declare const AnswerInput: Schema.Struct<{
|
|
|
9
9
|
readonly message: Schema.optional<Schema.String>;
|
|
10
10
|
readonly answers: Schema.optional<Schema.$Array<Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>>>;
|
|
11
11
|
}>;
|
|
12
|
-
/** What the person's tool runs on. */
|
|
13
12
|
export interface PersonToolPorts {
|
|
14
13
|
readonly answer: AnswerPorts;
|
|
15
14
|
readonly questions: QuestionPorts;
|
|
16
|
-
/** The permission requests sessions were told about and have not answered. */
|
|
17
15
|
readonly waiting: Waiting;
|
|
18
16
|
}
|
|
19
|
-
/** Adds `courier_answer`. */
|
|
20
17
|
export declare function addPersonTools(tools: ToolEditor, ports: PersonToolPorts): void;
|
package/dist/person/tools.js
CHANGED
|
@@ -16,7 +16,6 @@ export const AnswerInput = Schema.Struct({
|
|
|
16
16
|
"or a list of labels where the question allows several.",
|
|
17
17
|
})),
|
|
18
18
|
});
|
|
19
|
-
// One tool answers both: a question's id is the plugin's own, a permission request's OpenCode's.
|
|
20
19
|
async function answerRequest(ports, callerID, input, call) {
|
|
21
20
|
const { sessionID, requestID } = input;
|
|
22
21
|
if (isQuestion(requestID)) {
|
|
@@ -38,7 +37,6 @@ async function answerRequest(ports, callerID, input, call) {
|
|
|
38
37
|
...(call ? { call } : {}),
|
|
39
38
|
});
|
|
40
39
|
}
|
|
41
|
-
/** Adds `courier_answer`. */
|
|
42
40
|
export function addPersonTools(tools, ports) {
|
|
43
41
|
tools.add({
|
|
44
42
|
name: "courier_answer",
|
|
@@ -55,7 +53,7 @@ export function addPersonTools(tools, ports) {
|
|
|
55
53
|
execute: async (input, context) => {
|
|
56
54
|
const call = { messageID: context.messageID, callID: context.id };
|
|
57
55
|
const result = await answerRequest(ports, context.sessionID, input, call).catch((error) => {
|
|
58
|
-
//
|
|
56
|
+
// rethrown untouched, a plain decline ends the step (api-notes)
|
|
59
57
|
if (isDeclined(error))
|
|
60
58
|
throw error;
|
|
61
59
|
return rethrow("courier_answer")(error);
|
package/dist/person/tui.d.ts
CHANGED
|
@@ -5,10 +5,6 @@ type FormInfo = NonNullable<ReturnType<Context["data"]["session"]["form"]["list"
|
|
|
5
5
|
type FormField = FormInfo["fields"][number];
|
|
6
6
|
type FormAnswer = Parameters<Context["data"]["session"]["form"]["reply"]>[0]["answer"];
|
|
7
7
|
type FormValue = FormAnswer[string];
|
|
8
|
-
/**
|
|
9
|
-
* Something a session of the tree waits on the person for; `key` is unique across kinds. `prompted` marks a permission
|
|
10
|
-
* request the top session's courier_answer prompt asks about.
|
|
11
|
-
*/
|
|
12
8
|
export type Ask = {
|
|
13
9
|
readonly key: string;
|
|
14
10
|
readonly session: TreeSession;
|
|
@@ -23,33 +19,16 @@ export type Ask = {
|
|
|
23
19
|
readonly kind: "form";
|
|
24
20
|
readonly form: FormInfo;
|
|
25
21
|
});
|
|
26
|
-
/** The command that opens the inbox, from its key, its slash command, the palette or the footer. */
|
|
27
22
|
export declare const INBOX_COMMAND = "courier.asks";
|
|
28
|
-
/** How long after the first of a burst of asks the inbox reads the tree again, to sound the new ones. */
|
|
29
23
|
export declare const SETTLE_MS = 250;
|
|
30
|
-
/** The top session, at depth 0, as its own permission requests name it. */
|
|
31
24
|
export declare const topSession: (asks: Asks) => TreeSession;
|
|
32
|
-
/**
|
|
33
|
-
* The asks of a tree in the order the inbox lists them: the top session's own permission requests, then by the tree's
|
|
34
|
-
* order, each session's permissions, questions, then forms.
|
|
35
|
-
*/
|
|
36
25
|
export declare function asksOf(asks: Asks, forms: ReadonlyMap<string, ReadonlyArray<FormInfo>>): Ask[];
|
|
37
|
-
/** Where an ask comes from: the titles from the top session down to the asking one, as "root › child › grandchild", each as the TUI knows it now, else as spawned. */
|
|
38
26
|
export declare function pathOf(asks: Asks, sessionID: string, topTitle: string, currentTitle?: (sessionID: string) => string | undefined): string;
|
|
39
|
-
/** What an ask is, in one line. */
|
|
40
27
|
export declare function askTitle(ask: Ask): string;
|
|
41
|
-
/** The line under an ask: its depth (the top's own say so) and group, that a question's call was cut off, or that the top's courier_answer prompt asks for it. */
|
|
42
28
|
export declare function askFooter(ask: Ask): string;
|
|
43
|
-
/** Whether a form field applies, given the answers so far (a hidden field's default among them): its `when` conditions hold, as OpenCode reads them (a condition on an unanswered field is false; on a multiselect, `eq` means includes). */
|
|
44
29
|
export declare function active(field: FormField, answers: FormAnswer): boolean;
|
|
45
|
-
/** What is wrong with a field's answer by the limits OpenCode's form service checks on reply, worded as its form view does; undefined when it fits. */
|
|
46
30
|
export declare function misfit(field: FormField, value: FormValue): string | undefined;
|
|
47
|
-
/** The limits of a typed field as a prompt's placeholder: "1-5", "at least 3 characters", "email"; undefined without any. */
|
|
48
31
|
export declare function limitsOf(field: FormField): string | undefined;
|
|
49
|
-
/**
|
|
50
|
-
* Adds the asks inbox: what the open session's courier tree waits on the person for, read through the person's RPC and
|
|
51
|
-
* answered in dialogs, opened by its command, its open list read again on `follows`; a sound for each new ask.
|
|
52
|
-
*/
|
|
53
32
|
export declare function addAsksInbox(ctx: Context, follows: ReadonlySet<string>): {
|
|
54
33
|
open: (sessionID?: string | undefined) => Promise<void>;
|
|
55
34
|
cleanup: () => void;
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bounded `Set`s and `Map`s of remembered ids that drop their oldest entry past a bound. Functions
|
|
3
|
-
* on the built-ins, not a class, as older plugin copies call `.has` and `.add` on the shared ones.
|
|
4
|
-
*/
|
|
5
|
-
/** Adds to a bounded set, dropping the oldest entry past `max`; false when the value was there already. */
|
|
6
1
|
export declare function addBounded<T>(set: Set<T>, value: T, max: number): boolean;
|
|
7
|
-
/** Sets an entry of a bounded map, dropping the oldest entry past `max`; an existing key keeps its place. */
|
|
8
2
|
export declare function setBounded<K, V>(map: Map<K, V>, key: K, value: V, max: number): void;
|
|
9
|
-
/** How many handled event ids, and waiting or answered requests and forms, are remembered. */
|
|
10
3
|
export declare const SEEN_MAX = 1000;
|
|
11
|
-
/** Claims a value in one of the shared sets; false when it was claimed already. */
|
|
12
4
|
export declare const claim: (set: Set<string>, value: string) => boolean;
|
package/dist/platform/bounded.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bounded `Set`s and `Map`s of remembered ids that drop their oldest entry past a bound. Functions
|
|
3
|
-
* on the built-ins, not a class, as older plugin copies call `.has` and `.add` on the shared ones.
|
|
4
|
-
*/
|
|
5
|
-
/** Adds to a bounded set, dropping the oldest entry past `max`; false when the value was there already. */
|
|
6
1
|
export function addBounded(set, value, max) {
|
|
7
2
|
if (set.has(value))
|
|
8
3
|
return false;
|
|
@@ -11,13 +6,10 @@ export function addBounded(set, value, max) {
|
|
|
11
6
|
set.delete(set.values().next().value);
|
|
12
7
|
return true;
|
|
13
8
|
}
|
|
14
|
-
/** Sets an entry of a bounded map, dropping the oldest entry past `max`; an existing key keeps its place. */
|
|
15
9
|
export function setBounded(map, key, value, max) {
|
|
16
10
|
map.set(key, value);
|
|
17
11
|
if (map.size > max)
|
|
18
12
|
map.delete(map.keys().next().value);
|
|
19
13
|
}
|
|
20
|
-
/** How many handled event ids, and waiting or answered requests and forms, are remembered. */
|
|
21
14
|
export const SEEN_MAX = 1_000;
|
|
22
|
-
/** Claims a value in one of the shared sets; false when it was claimed already. */
|
|
23
15
|
export const claim = (set, value) => addBounded(set, value, SEEN_MAX);
|
|
@@ -1,42 +1,24 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin";
|
|
2
2
|
import type { Hub } from "./hub.js";
|
|
3
|
-
/** One of OpenCode's events, as a subscription yields it. */
|
|
4
3
|
export interface HostEvent {
|
|
5
4
|
readonly type: string;
|
|
6
5
|
readonly data?: unknown;
|
|
7
6
|
}
|
|
8
|
-
/** What following OpenCode's events needs of a member's ports: the stream, and a log. */
|
|
9
7
|
export interface EventPorts {
|
|
10
8
|
readonly event: Pick<Plugin.Context["event"], "subscribe">;
|
|
11
9
|
readonly log: (message: string) => void;
|
|
12
10
|
}
|
|
13
|
-
/** An event type `event.subscribe` can filter on: any of OpenCode's but `server.connected`, which a plugin never receives. */
|
|
14
11
|
export type EventType = NonNullable<NonNullable<Parameters<EventPorts["event"]["subscribe"]>[0]>["types"]>[number];
|
|
15
|
-
/** What a module does with OpenCode's events, by phase; each phase runs the modules in list order. */
|
|
16
12
|
export interface EventModule<P extends EventPorts> {
|
|
17
|
-
/** The event types it handles; without them, every event, so the subscription is not filtered. */
|
|
18
13
|
readonly types?: readonly EventType[];
|
|
19
|
-
/** For each event; the first to throw ends the phase. */
|
|
20
14
|
readonly handle?: (ports: P, event: HostEvent) => Promise<unknown> | undefined;
|
|
21
|
-
/** For each event, once the `handle` phase has ended, however it did; the first to throw ends this one too. */
|
|
22
15
|
readonly after?: (ports: P, event: HostEvent) => Promise<void> | undefined;
|
|
23
|
-
/** As a subscription starts; returns what to run once the stream is surely connected, if anything. */
|
|
24
16
|
readonly followed?: () => (() => void) | undefined;
|
|
25
|
-
/** Alongside each (re)subscription; the watcher's end waits for it. */
|
|
26
17
|
readonly subscribed?: (ports: P) => Promise<void>;
|
|
27
|
-
/** Once a subscription has ended or broken. */
|
|
28
18
|
readonly left?: () => void;
|
|
29
19
|
}
|
|
30
|
-
/** How long to wait before subscribing again after the event stream ended or broke. */
|
|
31
20
|
export declare const RESUBSCRIBE_MS = 5000;
|
|
32
|
-
/** How long after subscribing the stream is taken as connected, should no event come first: a plugin's reports no connection. */
|
|
33
21
|
export declare const CONNECTED_MS = 1000;
|
|
34
|
-
/** The types `modules` declare, sorted; undefined, every type, once one of them declares none. */
|
|
35
22
|
export declare function typesOf(modules: readonly EventModule<never>[]): EventType[] | undefined;
|
|
36
|
-
/** Follows OpenCode's events through `ports`, subscribing again after `retryMs` when a subscription ends, until `signal` aborts. */
|
|
37
23
|
export declare function follow<P extends EventPorts>(ports: P, modules: readonly EventModule<P>[], signal: AbortSignal, retryMs?: number, connectedMs?: number): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Makes the hub start its subscriptions to OpenCode's events with this copy's modules, the copy loaded
|
|
40
|
-
* last; `join` starts them. The second is a standby, connected before the first's member leaves.
|
|
41
|
-
*/
|
|
42
24
|
export declare function watchFromHub<P extends EventPorts>(hub: Pick<Hub, "subscribe">, modules: readonly EventModule<P>[], retryMs?: number): void;
|
package/dist/platform/events.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
/** How long to wait before subscribing again after the event stream ended or broke. */
|
|
2
1
|
export const RESUBSCRIBE_MS = 5_000;
|
|
3
|
-
|
|
2
|
+
// a plugin's event stream never says it connected (api-notes)
|
|
4
3
|
export const CONNECTED_MS = 1_000;
|
|
5
4
|
const pause = (ms, signal) => new Promise((resolve) => {
|
|
6
5
|
const timer = setTimeout(done, ms);
|
|
@@ -12,10 +11,6 @@ const pause = (ms, signal) => new Promise((resolve) => {
|
|
|
12
11
|
signal.addEventListener("abort", done);
|
|
13
12
|
});
|
|
14
13
|
const ENDED = { done: true, value: undefined };
|
|
15
|
-
/**
|
|
16
|
-
* `events` until it ends or `signal` aborts, the stream left open; each wait hears the abort only while
|
|
17
|
-
* it waits, so a watcher that follows events for the life of the process holds nothing per event.
|
|
18
|
-
*/
|
|
19
14
|
const until = (events, signal) => ({
|
|
20
15
|
[Symbol.asyncIterator]: () => ({
|
|
21
16
|
next: () => new Promise((resolve, reject) => {
|
|
@@ -23,7 +18,6 @@ const until = (events, signal) => ({
|
|
|
23
18
|
return resolve(ENDED);
|
|
24
19
|
const stop = () => resolve(ENDED);
|
|
25
20
|
signal.addEventListener("abort", stop, { once: true });
|
|
26
|
-
// Once the signal has aborted, whatever the stream ends this wait with is not wanted.
|
|
27
21
|
events.next().then((next) => {
|
|
28
22
|
signal.removeEventListener("abort", stop);
|
|
29
23
|
resolve(next);
|
|
@@ -34,7 +28,6 @@ const until = (events, signal) => ({
|
|
|
34
28
|
}),
|
|
35
29
|
}),
|
|
36
30
|
});
|
|
37
|
-
/** The types `modules` declare, sorted; undefined, every type, once one of them declares none. */
|
|
38
31
|
export function typesOf(modules) {
|
|
39
32
|
const types = new Set();
|
|
40
33
|
for (const module of modules) {
|
|
@@ -45,7 +38,6 @@ export function typesOf(modules) {
|
|
|
45
38
|
}
|
|
46
39
|
return [...types].sort((a, b) => Number(a > b) - Number(a < b));
|
|
47
40
|
}
|
|
48
|
-
/** One event through the `handle` phase, then, however that ended, the `after` phase. */
|
|
49
41
|
async function handle(ports, modules, event) {
|
|
50
42
|
try {
|
|
51
43
|
for (const module of modules) {
|
|
@@ -62,7 +54,6 @@ async function handle(ports, modules, event) {
|
|
|
62
54
|
}
|
|
63
55
|
}
|
|
64
56
|
}
|
|
65
|
-
/** Handles one event, logging a failure rather than ending the watch. */
|
|
66
57
|
function handleLogged(ports, modules, event) {
|
|
67
58
|
return handle(ports, modules, event).catch((error) => {
|
|
68
59
|
const data = event.data;
|
|
@@ -70,7 +61,6 @@ function handleLogged(ports, modules, event) {
|
|
|
70
61
|
ports.log(`courier watch: could not handle ${event.type} of ${sessionID}: ${String(error)}`);
|
|
71
62
|
});
|
|
72
63
|
}
|
|
73
|
-
/** Runs each module's `subscribed` alongside the new subscription, kept in `alongside` until done. */
|
|
74
64
|
function subscribed(ports, modules, alongside) {
|
|
75
65
|
for (const module of modules) {
|
|
76
66
|
if (!module.subscribed)
|
|
@@ -82,13 +72,11 @@ function subscribed(ports, modules, alongside) {
|
|
|
82
72
|
alongside.add(running);
|
|
83
73
|
}
|
|
84
74
|
}
|
|
85
|
-
/** One subscription, its events through `modules`' phases, until its stream ends or breaks or `signal` aborts. */
|
|
86
75
|
async function subscription(ports, modules, signal, alongside, connectedMs) {
|
|
87
76
|
let following = false;
|
|
88
77
|
let events;
|
|
89
78
|
let first = [];
|
|
90
79
|
let timer;
|
|
91
|
-
// The stream is surely connected at its first event, filtered or not, or `connectedMs` after subscribing.
|
|
92
80
|
const connected = () => {
|
|
93
81
|
clearTimeout(timer);
|
|
94
82
|
for (const run of first.splice(0))
|
|
@@ -105,7 +93,7 @@ async function subscription(ports, modules, signal, alongside, connectedMs) {
|
|
|
105
93
|
subscribed(ports, modules, alongside);
|
|
106
94
|
for await (const event of until(events, signal)) {
|
|
107
95
|
connected();
|
|
108
|
-
//
|
|
96
|
+
// a host may ignore the types filter (api-notes)
|
|
109
97
|
if (wanted && !wanted.has(event.type))
|
|
110
98
|
continue;
|
|
111
99
|
await handleLogged(ports, modules, event);
|
|
@@ -121,12 +109,10 @@ async function subscription(ports, modules, signal, alongside, connectedMs) {
|
|
|
121
109
|
if (following)
|
|
122
110
|
for (const module of modules)
|
|
123
111
|
module.left?.();
|
|
124
|
-
//
|
|
125
|
-
// while a location closes, and the leave waiting for this watcher must not wait for that too.
|
|
112
|
+
// not awaited: closing waits on OpenCode, stuck while a location closes
|
|
126
113
|
void events?.return?.().catch(() => { });
|
|
127
114
|
}
|
|
128
115
|
}
|
|
129
|
-
/** Follows OpenCode's events through `ports`, subscribing again after `retryMs` when a subscription ends, until `signal` aborts. */
|
|
130
116
|
export async function follow(ports, modules, signal, retryMs = RESUBSCRIBE_MS, connectedMs = CONNECTED_MS) {
|
|
131
117
|
const alongside = new Set();
|
|
132
118
|
while (!signal.aborted) {
|
|
@@ -136,16 +122,11 @@ export async function follow(ports, modules, signal, retryMs = RESUBSCRIBE_MS, c
|
|
|
136
122
|
}
|
|
137
123
|
await Promise.allSettled(alongside);
|
|
138
124
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Makes the hub start its subscriptions to OpenCode's events with this copy's modules, the copy loaded
|
|
141
|
-
* last; `join` starts them. The second is a standby, connected before the first's member leaves.
|
|
142
|
-
*/
|
|
143
125
|
export function watchFromHub(hub, modules, retryMs = RESUBSCRIBE_MS) {
|
|
144
126
|
const types = typesOf(modules);
|
|
145
127
|
hub.subscribe = (member) => {
|
|
146
128
|
const stop = new AbortController();
|
|
147
129
|
member.log(`courier watch: following ${types ? types.join(", ") : "every event"} through ${member.directory}`);
|
|
148
|
-
// A member of any copy runs them: every hub-v2 member's `watch` ports are the same shape.
|
|
149
130
|
return { member, stop, done: follow(member.watch, modules, stop.signal, retryMs) };
|
|
150
131
|
};
|
|
151
132
|
}
|