opencode-courier 0.4.1 → 0.6.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 +38 -25
- package/dist/index.js +7 -4
- package/dist/person/form.d.ts +4 -1
- package/dist/person/form.js +5 -0
- package/dist/person/index.d.ts +2 -1
- package/dist/person/index.js +1 -0
- package/dist/person/permission.d.ts +37 -2
- package/dist/person/permission.js +91 -17
- package/dist/person/ports.d.ts +8 -2
- package/dist/person/rpc-handlers.d.ts +37 -30
- package/dist/person/rpc-handlers.js +26 -16
- package/dist/person/rpc.d.ts +43 -24
- package/dist/person/rpc.js +4 -3
- package/dist/person/text.d.ts +5 -0
- package/dist/person/text.js +21 -19
- package/dist/person/tools.d.ts +1 -1
- package/dist/person/tools.js +23 -12
- package/dist/person/tui.d.ts +23 -12
- package/dist/tree/domain.d.ts +23 -0
- package/dist/tree/domain.js +26 -0
- package/dist/tree/events.d.ts +2 -2
- package/dist/tree/events.js +5 -3
- package/dist/tree/index.d.ts +1 -1
- package/dist/tree/panel.d.ts +7 -2
- package/dist/tree/panel.js +19 -16
- package/dist/tree/ports.d.ts +7 -0
- package/dist/tree/roster.d.ts +5 -0
- package/dist/tree/roster.js +5 -3
- package/dist/tree/rpc-handlers.d.ts +45 -5
- package/dist/tree/rpc-handlers.js +30 -1
- package/dist/tree/rpc.d.ts +79 -19
- package/dist/tree/rpc.js +20 -8
- package/dist/tree/subtree.d.ts +40 -9
- package/dist/tree/subtree.js +40 -19
- package/dist/tree/summary.d.ts +2 -2
- package/dist/tree/summary.js +8 -16
- package/dist/tree/text.js +4 -1
- package/dist/tree/tools.js +2 -1
- package/dist/tree/tui.d.ts +12 -5
- package/dist/tui.js +585 -176
- package/package.json +4 -4
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { lineage, subtree } from "../tree/index.js";
|
|
2
|
-
import { pendingOf } from "./permission.js";
|
|
2
|
+
import { ownRequests, pendingOf } from "./permission.js";
|
|
3
3
|
import { answerForPerson, pendingQuestions } from "./question/index.js";
|
|
4
4
|
import { personRpc } from "./rpc.js";
|
|
5
5
|
/**
|
|
6
|
-
* The permission requests and questions waiting in the courier tree a session is in,
|
|
7
|
-
* top of its lineage down, so each session of the tree sees the same inbox.
|
|
6
|
+
* The permission requests and questions waiting in the courier tree a session is in, the top's own requests first, read from the
|
|
7
|
+
* roster from the top of its lineage down, so each session of the tree sees the same inbox.
|
|
8
8
|
*/
|
|
9
9
|
export async function asks(ports, sessionID) {
|
|
10
10
|
const { storage } = ports.questions;
|
|
11
11
|
const top = (await lineage(storage, sessionID)).at(-1)?.parentID ?? sessionID;
|
|
12
|
-
const { nodes
|
|
12
|
+
const { nodes } = await subtree(storage, top, { now: ports.questions.now() });
|
|
13
|
+
const own = nodes.length ? await ownRequests(ports.permissions(), top).catch(() => []) : [];
|
|
13
14
|
const waiting = await Promise.all(nodes.map(({ entry }) => Promise.all([
|
|
14
15
|
pendingOf(ports.permissions(), entry.sessionID).catch(() => []),
|
|
15
16
|
pendingQuestions(storage, entry.sessionID).catch(() => []),
|
|
@@ -23,24 +24,33 @@ export async function asks(ports, sessionID) {
|
|
|
23
24
|
depth,
|
|
24
25
|
...(entry.group ? { group: entry.group } : {}),
|
|
25
26
|
})),
|
|
26
|
-
permissions:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
]
|
|
36
|
-
|
|
27
|
+
permissions: [
|
|
28
|
+
...own.map(({ request, passesOn }) => ({
|
|
29
|
+
sessionID: top,
|
|
30
|
+
requestID: request.id,
|
|
31
|
+
action: request.action,
|
|
32
|
+
resources: [...request.resources],
|
|
33
|
+
...(request.save?.length ? { save: [...request.save] } : {}),
|
|
34
|
+
...(passesOn ? { passesOn } : {}),
|
|
35
|
+
})),
|
|
36
|
+
...nodes.flatMap(({ entry }, index) => waiting[index][0].flatMap((pending) => pending.type === "permission"
|
|
37
|
+
? [
|
|
38
|
+
{
|
|
39
|
+
sessionID: entry.sessionID,
|
|
40
|
+
requestID: pending.requestID,
|
|
41
|
+
action: pending.action,
|
|
42
|
+
resources: pending.resources,
|
|
43
|
+
...(pending.save ? { save: pending.save } : {}),
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: [])),
|
|
47
|
+
],
|
|
37
48
|
questions: nodes.flatMap(({ entry }, index) => waiting[index][1].map(({ requestID, questions, stopped }) => ({
|
|
38
49
|
sessionID: entry.sessionID,
|
|
39
50
|
requestID,
|
|
40
51
|
questions,
|
|
41
52
|
...(stopped ? { stopped } : {}),
|
|
42
53
|
}))),
|
|
43
|
-
...(truncated ? { truncated: ports.maxTotal } : {}),
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
/** Passes the person's answers to a question on, as courier_answer does; a refusal, such as answers that do not fit, comes back as `problem`. */
|
package/dist/person/rpc.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ declare const Session: Schema.Struct<{
|
|
|
8
8
|
readonly depth: Schema.Int;
|
|
9
9
|
readonly group: Schema.optionalKey<Schema.String>;
|
|
10
10
|
}>;
|
|
11
|
-
/** A permission request a session of the tree waits on. */
|
|
11
|
+
/** A permission request a session of the tree, the top among them, waits on. */
|
|
12
12
|
declare const Permission: Schema.Struct<{
|
|
13
13
|
readonly sessionID: Schema.String;
|
|
14
14
|
readonly requestID: Schema.String;
|
|
@@ -16,6 +16,11 @@ declare const Permission: Schema.Struct<{
|
|
|
16
16
|
readonly resources: Schema.$Array<Schema.String>;
|
|
17
17
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
18
18
|
readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
19
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
20
|
+
readonly passesOn: Schema.optionalKey<Schema.Struct<{
|
|
21
|
+
readonly sessionID: Schema.String;
|
|
22
|
+
readonly requestID: Schema.String;
|
|
23
|
+
}>>;
|
|
19
24
|
}>;
|
|
20
25
|
/** A question a session of the tree waits on; `stopped` when its call was cut off and the answer goes as a message. */
|
|
21
26
|
declare const Question: Schema.Struct<{
|
|
@@ -45,6 +50,7 @@ declare const Asks: Schema.Struct<{
|
|
|
45
50
|
readonly depth: Schema.Int;
|
|
46
51
|
readonly group: Schema.optionalKey<Schema.String>;
|
|
47
52
|
}>>;
|
|
53
|
+
/** The top session's own requests first, then those of the sessions below. */
|
|
48
54
|
readonly permissions: Schema.$Array<Schema.Struct<{
|
|
49
55
|
readonly sessionID: Schema.String;
|
|
50
56
|
readonly requestID: Schema.String;
|
|
@@ -52,6 +58,11 @@ declare const Asks: Schema.Struct<{
|
|
|
52
58
|
readonly resources: Schema.$Array<Schema.String>;
|
|
53
59
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
54
60
|
readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
61
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
62
|
+
readonly passesOn: Schema.optionalKey<Schema.Struct<{
|
|
63
|
+
readonly sessionID: Schema.String;
|
|
64
|
+
readonly requestID: Schema.String;
|
|
65
|
+
}>>;
|
|
55
66
|
}>>;
|
|
56
67
|
readonly questions: Schema.$Array<Schema.Struct<{
|
|
57
68
|
readonly sessionID: Schema.String;
|
|
@@ -67,8 +78,6 @@ declare const Asks: Schema.Struct<{
|
|
|
67
78
|
}>>;
|
|
68
79
|
readonly stopped: Schema.optionalKey<Schema.Boolean>;
|
|
69
80
|
}>>;
|
|
70
|
-
/** Set when the tree had more sessions than `maxTotal`, which were not read. */
|
|
71
|
-
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
72
81
|
}>;
|
|
73
82
|
export type Asks = typeof Asks.Type;
|
|
74
83
|
export type AskedQuestion = typeof Question.Type;
|
|
@@ -93,20 +102,7 @@ export declare const personRpc: {
|
|
|
93
102
|
readonly sessionID: Schema.String;
|
|
94
103
|
}>;
|
|
95
104
|
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
96
|
-
readonly
|
|
97
|
-
readonly sessionID: string;
|
|
98
|
-
readonly requestID: string;
|
|
99
|
-
readonly questions: readonly {
|
|
100
|
-
readonly options: readonly {
|
|
101
|
-
readonly label: string;
|
|
102
|
-
readonly description: string;
|
|
103
|
-
}[];
|
|
104
|
-
readonly question: string;
|
|
105
|
-
readonly header: string;
|
|
106
|
-
readonly multiple?: boolean | undefined;
|
|
107
|
-
}[];
|
|
108
|
-
readonly stopped?: boolean | undefined;
|
|
109
|
-
}[];
|
|
105
|
+
readonly top: string;
|
|
110
106
|
readonly sessions: readonly {
|
|
111
107
|
readonly sessionID: string;
|
|
112
108
|
readonly title: string;
|
|
@@ -120,10 +116,11 @@ export declare const personRpc: {
|
|
|
120
116
|
readonly requestID: string;
|
|
121
117
|
readonly action: string;
|
|
122
118
|
readonly save?: readonly string[] | undefined;
|
|
119
|
+
readonly passesOn?: {
|
|
120
|
+
readonly sessionID: string;
|
|
121
|
+
readonly requestID: string;
|
|
122
|
+
} | undefined;
|
|
123
123
|
}[];
|
|
124
|
-
readonly top: string;
|
|
125
|
-
readonly truncated?: number | undefined;
|
|
126
|
-
}, {
|
|
127
124
|
readonly questions: readonly {
|
|
128
125
|
readonly sessionID: string;
|
|
129
126
|
readonly requestID: string;
|
|
@@ -138,6 +135,8 @@ export declare const personRpc: {
|
|
|
138
135
|
}[];
|
|
139
136
|
readonly stopped?: boolean | undefined;
|
|
140
137
|
}[];
|
|
138
|
+
}, {
|
|
139
|
+
readonly top: string;
|
|
141
140
|
readonly sessions: readonly {
|
|
142
141
|
readonly sessionID: string;
|
|
143
142
|
readonly title: string;
|
|
@@ -151,9 +150,25 @@ export declare const personRpc: {
|
|
|
151
150
|
readonly requestID: string;
|
|
152
151
|
readonly action: string;
|
|
153
152
|
readonly save?: readonly string[] | undefined;
|
|
153
|
+
readonly passesOn?: {
|
|
154
|
+
readonly sessionID: string;
|
|
155
|
+
readonly requestID: string;
|
|
156
|
+
} | undefined;
|
|
157
|
+
}[];
|
|
158
|
+
readonly questions: readonly {
|
|
159
|
+
readonly sessionID: string;
|
|
160
|
+
readonly requestID: string;
|
|
161
|
+
readonly questions: readonly {
|
|
162
|
+
readonly options: readonly {
|
|
163
|
+
readonly label: string;
|
|
164
|
+
readonly description: string;
|
|
165
|
+
}[];
|
|
166
|
+
readonly question: string;
|
|
167
|
+
readonly header: string;
|
|
168
|
+
readonly multiple?: boolean | undefined;
|
|
169
|
+
}[];
|
|
170
|
+
readonly stopped?: boolean | undefined;
|
|
154
171
|
}[];
|
|
155
|
-
readonly top: string;
|
|
156
|
-
readonly truncated?: number | undefined;
|
|
157
172
|
}> & Schema.Struct<{
|
|
158
173
|
/** The session at the top of the tree, which the sessions below were started from. */
|
|
159
174
|
readonly top: Schema.String;
|
|
@@ -166,6 +181,7 @@ export declare const personRpc: {
|
|
|
166
181
|
readonly depth: Schema.Int;
|
|
167
182
|
readonly group: Schema.optionalKey<Schema.String>;
|
|
168
183
|
}>>;
|
|
184
|
+
/** The top session's own requests first, then those of the sessions below. */
|
|
169
185
|
readonly permissions: Schema.$Array<Schema.Struct<{
|
|
170
186
|
readonly sessionID: Schema.String;
|
|
171
187
|
readonly requestID: Schema.String;
|
|
@@ -173,6 +189,11 @@ export declare const personRpc: {
|
|
|
173
189
|
readonly resources: Schema.$Array<Schema.String>;
|
|
174
190
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
175
191
|
readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
192
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
193
|
+
readonly passesOn: Schema.optionalKey<Schema.Struct<{
|
|
194
|
+
readonly sessionID: Schema.String;
|
|
195
|
+
readonly requestID: Schema.String;
|
|
196
|
+
}>>;
|
|
176
197
|
}>>;
|
|
177
198
|
readonly questions: Schema.$Array<Schema.Struct<{
|
|
178
199
|
readonly sessionID: Schema.String;
|
|
@@ -188,8 +209,6 @@ export declare const personRpc: {
|
|
|
188
209
|
}>>;
|
|
189
210
|
readonly stopped: Schema.optionalKey<Schema.Boolean>;
|
|
190
211
|
}>>;
|
|
191
|
-
/** Set when the tree had more sessions than `maxTotal`, which were not read. */
|
|
192
|
-
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
193
212
|
}>;
|
|
194
213
|
};
|
|
195
214
|
readonly answer: {
|
package/dist/person/rpc.js
CHANGED
|
@@ -14,7 +14,7 @@ const Session = Schema.Struct({
|
|
|
14
14
|
depth: Count,
|
|
15
15
|
group: Schema.optionalKey(Schema.String),
|
|
16
16
|
});
|
|
17
|
-
/** A permission request a session of the tree waits on. */
|
|
17
|
+
/** A permission request a session of the tree, the top among them, waits on. */
|
|
18
18
|
const Permission = Schema.Struct({
|
|
19
19
|
sessionID: Schema.String,
|
|
20
20
|
requestID: Schema.String,
|
|
@@ -22,6 +22,8 @@ const Permission = Schema.Struct({
|
|
|
22
22
|
resources: Schema.Array(Schema.String),
|
|
23
23
|
/** What "always" would allow from now on; absent when it is not offered. */
|
|
24
24
|
save: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
25
|
+
/** The request below that allowing this one passes on: the top's courier_answer prompt. */
|
|
26
|
+
passesOn: Schema.optionalKey(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String })),
|
|
25
27
|
});
|
|
26
28
|
/** One question of OpenCode's question tool. */
|
|
27
29
|
const Prompt = Schema.Struct({
|
|
@@ -43,10 +45,9 @@ const Asks = Schema.Struct({
|
|
|
43
45
|
top: Schema.String,
|
|
44
46
|
/** The sessions below the top, in the order a tree is drawn. */
|
|
45
47
|
sessions: Schema.Array(Session),
|
|
48
|
+
/** The top session's own requests first, then those of the sessions below. */
|
|
46
49
|
permissions: Schema.Array(Permission),
|
|
47
50
|
questions: Schema.Array(Question),
|
|
48
|
-
/** Set when the tree had more sessions than `maxTotal`, which were not read. */
|
|
49
|
-
truncated: Schema.optionalKey(Count),
|
|
50
51
|
});
|
|
51
52
|
/** What answering a question did; `problem` says why nothing was passed on, when it was refused. */
|
|
52
53
|
const Answered = Schema.Struct({
|
package/dist/person/text.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare function answerText(result: AnswerResult): string;
|
|
|
10
10
|
* rejection, and the child would never report back; with a message, the call fails and it carries on.
|
|
11
11
|
*/
|
|
12
12
|
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
|
+
export declare function refusedText(sessionID: string, requestID: string, reason: string): string;
|
|
13
18
|
/** How a notice names where the asking session came from, to the session at the top of its lineage. */
|
|
14
19
|
export declare const origin: (startedBy?: string) => string;
|
|
15
20
|
/** For a notice that a session's turn ended without it finishing. */
|
package/dist/person/text.js
CHANGED
|
@@ -12,8 +12,10 @@ export function answerText(result) {
|
|
|
12
12
|
if (!result.answered)
|
|
13
13
|
return (`${result.sessionID} no longer waits on question ${result.requestID}: it was answered some other way, or the ` +
|
|
14
14
|
"session stopped waiting. Nothing was passed on; tell the person their answer is not needed.");
|
|
15
|
+
if ("reply" in result && result.reply === "reject")
|
|
16
|
+
return `Passed on reject for request ${result.requestID} of ${result.sessionID}; its call fails and it carries on, and reports back with courier_send. ${END_TURN}`;
|
|
15
17
|
if ("reply" in result)
|
|
16
|
-
return `
|
|
18
|
+
return `Allowed in your session, by the person in OpenCode's prompt or by your rules, and passed on ${result.reply} for request ${result.requestID} of ${result.sessionID}, which carries on and reports back with courier_send. ${END_TURN}`;
|
|
17
19
|
const how = result.by === "message" ? " as a message, since its question had been cut off" : "";
|
|
18
20
|
return `Passed the answers to question ${result.requestID} on to ${result.sessionID}${how}; it carries on and reports back with courier_send. ${END_TURN}`;
|
|
19
21
|
}
|
|
@@ -23,16 +25,16 @@ export function answerText(result) {
|
|
|
23
25
|
* rejection, and the child would never report back; with a message, the call fails and it carries on.
|
|
24
26
|
*/
|
|
25
27
|
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
|
+
export function refusedText(sessionID, requestID, reason) {
|
|
33
|
+
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
|
+
}
|
|
26
35
|
const MAX_RESOURCES = 20;
|
|
27
36
|
const MAX_RESOURCE_LENGTH = 300;
|
|
28
37
|
const clip = (text) => (text.length > MAX_RESOURCE_LENGTH ? `${text.slice(0, MAX_RESOURCE_LENGTH - 3)}...` : text);
|
|
29
|
-
function alwaysChoice(request) {
|
|
30
|
-
const save = request.save ?? [];
|
|
31
|
-
if (!save.length)
|
|
32
|
-
return [];
|
|
33
|
-
const scope = save.length === 1 && save[0] === "*" ? `every ${request.action} request` : `requests matching ${save.join(", ")}`;
|
|
34
|
-
return [`- always: allow it, and from now on ${scope} in this project`];
|
|
35
|
-
}
|
|
36
38
|
/** How a notice names where the asking session came from, to the session at the top of its lineage. */
|
|
37
39
|
export const origin = (startedBy) => startedBy ? `which ${startedBy} started with courier_spawn, a session started from yours,` : "which you started with courier_spawn,";
|
|
38
40
|
/** For a notice that a session's turn ended without it finishing. */
|
|
@@ -51,21 +53,21 @@ export function permissionNotice(title, request, startedBy, native = false) {
|
|
|
51
53
|
...(resources.length ? ["On:", ...resources] : []),
|
|
52
54
|
...(request.message ? [`Note: ${request.message}`] : []),
|
|
53
55
|
"",
|
|
54
|
-
...(native ?
|
|
55
|
-
"- once: allow this request only",
|
|
56
|
-
...alwaysChoice(request),
|
|
57
|
-
"- reject: refuse it, with a reason if they give one; the session's call fails and it carries on",
|
|
58
|
-
`When they have chosen, call courier_answer with sessionID "${request.sessionID}", requestID "${request.id}", reply set to their choice and, with reject, message set to their reason.`,
|
|
56
|
+
...(native ? nativePermission(request) : permissionAsk(request)),
|
|
59
57
|
].join("\n");
|
|
60
58
|
}
|
|
61
|
-
const
|
|
62
|
-
"
|
|
59
|
+
const permissionAsk = (request) => [
|
|
60
|
+
"You cannot decide this: the person you are working with does, in OpenCode's own permission prompt, which opens in your session when you propose once. Do not ask them in your reply or with your question tool first; propose one of these:",
|
|
61
|
+
"- once: allow this request only; OpenCode first asks the person in your session, under your permission rules, and it is passed on only once they have allowed it",
|
|
62
|
+
"- reject: refuse it, with a reason if the person gave one; the session's call fails and it carries on",
|
|
63
|
+
`Call courier_answer with sessionID "${request.sessionID}", requestID "${request.id}", reply set to the choice and, with reject, message set to the reason.`,
|
|
64
|
+
"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.",
|
|
63
65
|
];
|
|
64
|
-
/** For a native child's request, which OpenCode shows the person itself, in the top session's view. */
|
|
65
|
-
const
|
|
66
|
+
/** For a native child's request, which OpenCode shows the person itself, in the top session's view: nothing to call. */
|
|
67
|
+
const nativePermission = (request) => [
|
|
66
68
|
"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.",
|
|
67
|
-
"Do not ask them yourself, with your question tool or in your reply, and do not
|
|
68
|
-
|
|
69
|
+
"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
|
+
`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.`,
|
|
69
71
|
];
|
|
70
72
|
/** What the session told about a permission request is told once it is answered without courier_answer. */
|
|
71
73
|
export function permissionSettledNotice(title, requestID, reply) {
|
package/dist/person/tools.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { type QuestionPorts } from "./question/index.js";
|
|
|
5
5
|
export declare const AnswerInput: Schema.Struct<{
|
|
6
6
|
readonly sessionID: Schema.String;
|
|
7
7
|
readonly requestID: Schema.String;
|
|
8
|
-
readonly reply: Schema.optional<Schema.Literals<readonly ["once", "
|
|
8
|
+
readonly reply: Schema.optional<Schema.Literals<readonly ["once", "reject"]>>;
|
|
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
|
}>;
|
package/dist/person/tools.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { rethrow } from "../kernel/tool.js";
|
|
3
|
-
import { answer,
|
|
3
|
+
import { answer, isDeclined, PROPOSALS } from "./permission.js";
|
|
4
4
|
import { answerQuestion, isQuestion } from "./question/index.js";
|
|
5
5
|
import { answerText } from "./text.js";
|
|
6
6
|
export const AnswerInput = Schema.Struct({
|
|
7
7
|
sessionID: Schema.String.annotate({ description: "The session you started that is waiting, as its notice names it." }),
|
|
8
8
|
requestID: Schema.String.annotate({ description: "The request id from the notice." }),
|
|
9
|
-
reply: Schema.optional(Schema.Literals(
|
|
10
|
-
description: "For a permission request: the
|
|
9
|
+
reply: Schema.optional(Schema.Literals(PROPOSALS).annotate({
|
|
10
|
+
description: "For a permission request: the answer you propose, once or reject. With once, OpenCode first asks the person in " +
|
|
11
|
+
"your session, and it is passed on only once they have allowed it; always is not granted through this tool.",
|
|
11
12
|
})),
|
|
12
|
-
message: Schema.optional(Schema.String.annotate({ description: "For a permission request, with reject: the
|
|
13
|
+
message: Schema.optional(Schema.String.annotate({ description: "For a permission request, with reject: the reason, passed on with the refusal." })),
|
|
13
14
|
answers: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Array(Schema.String)])).annotate({
|
|
14
15
|
description: "For a question: one entry per question, in order, each the label the person chose or the text they gave, " +
|
|
15
16
|
"or a list of labels where the question allows several.",
|
|
16
17
|
})),
|
|
17
18
|
});
|
|
18
19
|
// One tool answers both: a question's id is the plugin's own, a permission request's OpenCode's.
|
|
19
|
-
async function answerRequest(ports, callerID, input) {
|
|
20
|
+
async function answerRequest(ports, callerID, input, call) {
|
|
20
21
|
const { sessionID, requestID } = input;
|
|
21
22
|
if (isQuestion(requestID)) {
|
|
22
23
|
if (input.reply !== undefined || input.message !== undefined)
|
|
@@ -26,14 +27,15 @@ async function answerRequest(ports, callerID, input) {
|
|
|
26
27
|
return answerQuestion(ports.questions, callerID, { sessionID, requestID, answers: input.answers });
|
|
27
28
|
}
|
|
28
29
|
if (input.answers !== undefined)
|
|
29
|
-
throw new Error(`${requestID} is a permission request: pass the
|
|
30
|
+
throw new Error(`${requestID} is a permission request: pass the answer you propose in reply (once or reject), not answers.`);
|
|
30
31
|
if (input.reply === undefined)
|
|
31
|
-
throw new Error(`${requestID} is a permission request: reply is required, once
|
|
32
|
+
throw new Error(`${requestID} is a permission request: reply is required, once or reject.`);
|
|
32
33
|
return answer(ports.answer, ports.waiting, callerID, {
|
|
33
34
|
sessionID,
|
|
34
35
|
requestID,
|
|
35
36
|
reply: input.reply,
|
|
36
37
|
...(input.message !== undefined ? { message: input.message } : {}),
|
|
38
|
+
...(call ? { call } : {}),
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
/** Adds `courier_answer`. */
|
|
@@ -42,13 +44,22 @@ export function addPersonTools(tools, ports) {
|
|
|
42
44
|
name: "courier_answer",
|
|
43
45
|
options: { codemode: false },
|
|
44
46
|
description: "Pass on the answer to a permission request or a question that a session you started with courier_spawn (or " +
|
|
45
|
-
"one started from it) waits on, after a notice from it named the request.
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
47
|
+
"one started from it) waits on, after a notice from it named the request. For a permission request, reply is " +
|
|
48
|
+
"the answer you propose, once or reject: proposing is not deciding. With once, OpenCode asks the person you are " +
|
|
49
|
+
"working with in your session, as its own permission prompt for what that session asked, under your permission " +
|
|
50
|
+
"rules, and the call returns once they have answered; what they allow is passed on, and a refusal with a reason " +
|
|
51
|
+
"fails this call and is passed on with the reason. Do not ask them in your reply or with your question tool first. " +
|
|
52
|
+
"For a question, answers is what the person chose: ask them first, with your question tool if you have one, and " +
|
|
53
|
+
"never answer for them. The session carries on once it has the answer.",
|
|
49
54
|
input: AnswerInput,
|
|
50
55
|
execute: async (input, context) => {
|
|
51
|
-
const
|
|
56
|
+
const call = { messageID: context.messageID, callID: context.id };
|
|
57
|
+
const result = await answerRequest(ports, context.sessionID, input, call).catch((error) => {
|
|
58
|
+
// A plain rejection of the caller's prompt ends its step, as OpenCode's own tools do: it goes back as it is.
|
|
59
|
+
if (isDeclined(error))
|
|
60
|
+
throw error;
|
|
61
|
+
return rethrow("courier_answer")(error);
|
|
62
|
+
});
|
|
52
63
|
return { content: answerText(result), metadata: result };
|
|
53
64
|
},
|
|
54
65
|
});
|
package/dist/person/tui.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin/tui";
|
|
2
|
-
import type { AttentionSoundName } from "@opencode/plugin/tui/context";
|
|
3
2
|
import { type AskedPermission, type AskedQuestion, type Asks, type TreeSession } from "./rpc.js";
|
|
4
3
|
type Context = Plugin.Context;
|
|
5
4
|
type FormInfo = NonNullable<ReturnType<Context["data"]["session"]["form"]["list"]>>[number];
|
|
6
5
|
type FormField = FormInfo["fields"][number];
|
|
7
6
|
type FormAnswer = Parameters<Context["data"]["session"]["form"]["reply"]>[0]["answer"];
|
|
8
|
-
|
|
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
|
+
*/
|
|
9
12
|
export type Ask = {
|
|
10
13
|
readonly key: string;
|
|
11
14
|
readonly session: TreeSession;
|
|
12
15
|
} & ({
|
|
13
16
|
readonly kind: "permission";
|
|
14
17
|
readonly request: AskedPermission;
|
|
18
|
+
readonly prompted?: boolean;
|
|
15
19
|
} | {
|
|
16
20
|
readonly kind: "question";
|
|
17
21
|
readonly question: AskedQuestion;
|
|
@@ -23,23 +27,30 @@ export type Ask = {
|
|
|
23
27
|
export declare const INBOX_COMMAND = "courier.asks";
|
|
24
28
|
/** How long after the first of a burst of asks the inbox reads the tree again, to sound the new ones. */
|
|
25
29
|
export declare const SETTLE_MS = 250;
|
|
26
|
-
/** The
|
|
30
|
+
/** The top session, at depth 0, as its own permission requests name it. */
|
|
31
|
+
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
|
+
*/
|
|
27
36
|
export declare function asksOf(asks: Asks, forms: ReadonlyMap<string, ReadonlyArray<FormInfo>>): Ask[];
|
|
28
|
-
/** Where an ask comes from: the titles from the top session down to the asking one, as "root › child › grandchild". */
|
|
29
|
-
export declare function pathOf(asks: Asks, sessionID: string, topTitle: string): string;
|
|
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
|
+
export declare function pathOf(asks: Asks, sessionID: string, topTitle: string, currentTitle?: (sessionID: string) => string | undefined): string;
|
|
30
39
|
/** What an ask is, in one line. */
|
|
31
40
|
export declare function askTitle(ask: Ask): string;
|
|
32
|
-
/** The line under an ask: its depth and group,
|
|
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. */
|
|
33
42
|
export declare function askFooter(ask: Ask): string;
|
|
34
|
-
/**
|
|
35
|
-
export declare
|
|
36
|
-
/**
|
|
37
|
-
export declare function
|
|
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
|
+
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
|
+
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
|
+
export declare function limitsOf(field: FormField): string | undefined;
|
|
38
49
|
/**
|
|
39
50
|
* Adds the asks inbox: what the open session's courier tree waits on the person for, read through the person's RPC and
|
|
40
|
-
* answered in dialogs, opened by its command
|
|
51
|
+
* answered in dialogs, opened by its command, its open list read again on `follows`; a sound for each new ask.
|
|
41
52
|
*/
|
|
42
|
-
export declare function addAsksInbox(ctx: Context): {
|
|
53
|
+
export declare function addAsksInbox(ctx: Context, follows: ReadonlySet<string>): {
|
|
43
54
|
open: (sessionID?: string | undefined) => Promise<void>;
|
|
44
55
|
cleanup: () => void;
|
|
45
56
|
};
|
package/dist/tree/domain.d.ts
CHANGED
|
@@ -26,6 +26,29 @@ export declare function readNativeChildren(options: unknown): {
|
|
|
26
26
|
type Times = Awaited<ReturnType<Context["session"]["get"]>>["time"];
|
|
27
27
|
/** Whether a session with these times runs: it has never finished a turn, or something reached it after the last one ended. */
|
|
28
28
|
export declare const busy: (time: Pick<Times, "updated" | "idle">) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a session a tree is read of is live: it runs (as the limits count it: busy, and its turn has not failed or
|
|
31
|
+
* been interrupted since its last prompt), waits on a request or form, or owes a report; one OpenCode no longer knows is not.
|
|
32
|
+
*/
|
|
33
|
+
export declare const isLive: (time: Pick<Times, "updated" | "idle"> | undefined, waits: number, report: {
|
|
34
|
+
readonly owes: boolean;
|
|
35
|
+
readonly ended: boolean;
|
|
36
|
+
} | undefined) => boolean;
|
|
37
|
+
/** A session of a tree read, as `cut` takes it: where it is in the tree, when it was started, and whether it is live. */
|
|
38
|
+
export interface Cuttable {
|
|
39
|
+
readonly sessionID: string;
|
|
40
|
+
readonly parentID: string;
|
|
41
|
+
readonly createdAt: number;
|
|
42
|
+
readonly live: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Which sessions a listing of at most `max` keeps, of a tree read in the order it is drawn: the live ones first, then
|
|
46
|
+
* the newest, each with the sessions above it so the tree still draws; `leftOut` counts the rest.
|
|
47
|
+
*/
|
|
48
|
+
export declare function cut<T extends Cuttable>(nodes: ReadonlyArray<T>, max: number): {
|
|
49
|
+
kept: T[];
|
|
50
|
+
leftOut: number;
|
|
51
|
+
};
|
|
29
52
|
/** A session started with courier_spawn, recorded under the session that started it. */
|
|
30
53
|
export interface RosterEntry {
|
|
31
54
|
readonly sessionID: string;
|
package/dist/tree/domain.js
CHANGED
|
@@ -25,6 +25,32 @@ export function readNativeChildren(options) {
|
|
|
25
25
|
}
|
|
26
26
|
/** Whether a session with these times runs: it has never finished a turn, or something reached it after the last one ended. */
|
|
27
27
|
export const busy = (time) => time.idle === undefined || millis(time.updated) > millis(time.idle);
|
|
28
|
+
/**
|
|
29
|
+
* Whether a session a tree is read of is live: it runs (as the limits count it: busy, and its turn has not failed or
|
|
30
|
+
* been interrupted since its last prompt), waits on a request or form, or owes a report; one OpenCode no longer knows is not.
|
|
31
|
+
*/
|
|
32
|
+
export const isLive = (time, waits, report) => time !== undefined && ((busy(time) && !report?.ended) || waits > 0 || (report?.owes ?? false));
|
|
33
|
+
/**
|
|
34
|
+
* Which sessions a listing of at most `max` keeps, of a tree read in the order it is drawn: the live ones first, then
|
|
35
|
+
* the newest, each with the sessions above it so the tree still draws; `leftOut` counts the rest.
|
|
36
|
+
*/
|
|
37
|
+
export function cut(nodes, max) {
|
|
38
|
+
const byID = new Map(nodes.map((node) => [node.sessionID, node]));
|
|
39
|
+
const ranked = nodes
|
|
40
|
+
.map((node, order) => ({ node, order }))
|
|
41
|
+
.sort((a, b) => Number(b.node.live) - Number(a.node.live) || b.node.createdAt - a.node.createdAt || a.order - b.order);
|
|
42
|
+
const kept = new Set();
|
|
43
|
+
for (const { node } of ranked) {
|
|
44
|
+
const chain = [];
|
|
45
|
+
for (let above = node; above && !kept.has(above.sessionID); above = byID.get(above.parentID))
|
|
46
|
+
chain.push(above.sessionID);
|
|
47
|
+
if (kept.size + chain.length > max)
|
|
48
|
+
continue;
|
|
49
|
+
for (const sessionID of chain)
|
|
50
|
+
kept.add(sessionID);
|
|
51
|
+
}
|
|
52
|
+
return { kept: nodes.filter((node) => kept.has(node.sessionID)), leftOut: nodes.length - kept.size };
|
|
53
|
+
}
|
|
28
54
|
/** `lineage` over roster entries already read. */
|
|
29
55
|
export function lineageIn(entries, sessionID) {
|
|
30
56
|
return lineageBy(bySession(entries), sessionID);
|
package/dist/tree/events.d.ts
CHANGED
|
@@ -51,8 +51,8 @@ export declare function notePrompt(ports: WatchPorts, event: SessionEvent & {
|
|
|
51
51
|
*/
|
|
52
52
|
export declare function noteInterrupted(ports: WatchPorts, event: SessionEvent): Promise<boolean>;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* Forgets what a deleted session leaves (report state, groups) and, unless isolated or a parent on the roster, its
|
|
55
|
+
* entry. Returns the sessions forgotten.
|
|
56
56
|
*/
|
|
57
57
|
export declare function noteDeleted(ports: WatchPorts, event: SessionEvent): Promise<string[]>;
|
|
58
58
|
/** A `Waits` asking `suppliers` one after another, cheapest first: the first that waits answers. */
|
package/dist/tree/events.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { envelope } from "../kernel/protocol.js";
|
|
2
2
|
import { claim, SEEN_MAX, setBounded } from "../platform/bounded.js";
|
|
3
|
-
import { forgetDeleted } from "./forget.js";
|
|
3
|
+
import { forgetChild, forgetDeleted } from "./forget.js";
|
|
4
4
|
import { hasCompleteGroup, leave, outIn } from "./group.js";
|
|
5
5
|
import { awaited, reportOf, stoppedSincePrompt } from "./report.js";
|
|
6
6
|
import { childStore, groupStore } from "./store.js";
|
|
@@ -88,8 +88,8 @@ export async function noteInterrupted(ports, event) {
|
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
91
|
+
* Forgets what a deleted session leaves (report state, groups) and, unless isolated or a parent on the roster, its
|
|
92
|
+
* entry. Returns the sessions forgotten.
|
|
93
93
|
*/
|
|
94
94
|
export async function noteDeleted(ports, event) {
|
|
95
95
|
const { sessionID } = event.data;
|
|
@@ -99,6 +99,8 @@ export async function noteDeleted(ports, event) {
|
|
|
99
99
|
const entry = parentID === undefined ? undefined : await children.entry(parentID, sessionID);
|
|
100
100
|
const member = entry && (await outIn(ports, entry));
|
|
101
101
|
await forgetDeleted(ports, sessionID, forgotten, member, event.created);
|
|
102
|
+
if (parentID !== undefined && entry && !entry.isolated && started.length === 0)
|
|
103
|
+
await forgetChild(ports.storage, parentID, sessionID);
|
|
102
104
|
return forgotten;
|
|
103
105
|
}
|
|
104
106
|
/** A `Waits` asking `suppliers` one after another, cheapest first: the first that waits answers. */
|
package/dist/tree/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { shapeContext } from "./context.js";
|
|
|
3
3
|
export { lineageIn, nativeRootOf, readLimits, readNativeChildren } from "./domain.js";
|
|
4
4
|
export { together, treeEvents, waitsOn } from "./events.js";
|
|
5
5
|
export { deliverReleased } from "./group.js";
|
|
6
|
-
export type { CleanupPorts, CourierPorts, Pending, PendingPrompt, RosterEntry, RosterStorage, SpawnGate, Waits, WatchPorts, WatchState, } from "./ports.js";
|
|
6
|
+
export type { CleanupPorts, CourierPorts, Pending, PendingPrompt, RosterEntry, RosterStorage, ShownForm, SpawnGate, Waits, WatchPorts, WatchState, } from "./ports.js";
|
|
7
7
|
export { allEntries, answeringTop, lineage, pruneExpired, RETENTION_MS } from "./roster.js";
|
|
8
8
|
export { subtree } from "./subtree.js";
|
|
9
9
|
export { registerTreeRpc } from "./rpc-handlers.js";
|
package/dist/tree/panel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CourierPorts, Pending } from "./ports.js";
|
|
1
|
+
import type { CourierPorts, Pending, ShownForm } from "./ports.js";
|
|
2
2
|
import type { Tree, Wait } from "./rpc.js";
|
|
3
3
|
type Messages = Awaited<ReturnType<CourierPorts["session"]["context"]>>;
|
|
4
4
|
/** The last tool call in a session's messages, and when it was made. */
|
|
@@ -8,6 +8,11 @@ export declare function lastTool(messages: Messages): {
|
|
|
8
8
|
} | undefined;
|
|
9
9
|
/** A request as the panel says what a paused session waits on: a permission request, or its first question. */
|
|
10
10
|
export declare const waitOf: (pending: Pending) => Wait;
|
|
11
|
-
/**
|
|
11
|
+
/** A form a session shows, as the panel says what it waits on. */
|
|
12
|
+
export declare const formWait: (form: ShownForm) => Wait;
|
|
13
|
+
/**
|
|
14
|
+
* The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, the live ones
|
|
15
|
+
* first, then the newest, each as `row` reads it; `truncated` counts the older ones left out.
|
|
16
|
+
*/
|
|
12
17
|
export declare function panel(ports: CourierPorts, rootID: string): Promise<Tree>;
|
|
13
18
|
export {};
|