opencode-courier 0.5.0 → 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 +20 -13
- package/dist/index.js +6 -3
- 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 +14 -0
- package/dist/person/permission.js +15 -0
- 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/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 +11 -4
- package/dist/tui.js +567 -174
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -18,7 +18,8 @@ var Permission = Schema.Struct({
|
|
|
18
18
|
requestID: Schema.String,
|
|
19
19
|
action: Schema.String,
|
|
20
20
|
resources: Schema.Array(Schema.String),
|
|
21
|
-
save: Schema.optionalKey(Schema.Array(Schema.String))
|
|
21
|
+
save: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
22
|
+
passesOn: Schema.optionalKey(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String }))
|
|
22
23
|
});
|
|
23
24
|
var Prompt = Schema.Struct({
|
|
24
25
|
question: Schema.String,
|
|
@@ -36,8 +37,7 @@ var Asks = Schema.Struct({
|
|
|
36
37
|
top: Schema.String,
|
|
37
38
|
sessions: Schema.Array(Session),
|
|
38
39
|
permissions: Schema.Array(Permission),
|
|
39
|
-
questions: Schema.Array(Question)
|
|
40
|
-
truncated: Schema.optionalKey(Count)
|
|
40
|
+
questions: Schema.Array(Question)
|
|
41
41
|
});
|
|
42
42
|
var Answered = Schema.Struct({
|
|
43
43
|
answered: Schema.Boolean,
|
|
@@ -66,12 +66,22 @@ var REJECTED = "Refused by the session that started you. Do without it, or repor
|
|
|
66
66
|
var QUESTION_FORM = "question";
|
|
67
67
|
var INBOX_COMMAND = "courier.asks";
|
|
68
68
|
var SETTLE_MS = 250;
|
|
69
|
+
var topSession = (asks) => ({
|
|
70
|
+
sessionID: asks.top,
|
|
71
|
+
title: "",
|
|
72
|
+
parentID: "",
|
|
73
|
+
depth: 0
|
|
74
|
+
});
|
|
69
75
|
function asksOf(asks, forms) {
|
|
70
|
-
|
|
76
|
+
const prompting = new Set(asks.permissions.flatMap((request) => request.sessionID === asks.top && request.passesOn ? [request.passesOn.requestID] : []));
|
|
77
|
+
return [topSession(asks), ...asks.sessions].flatMap((session) => [...asks.permissions.filter((request) => request.sessionID === session.sessionID).map((request) => ({
|
|
71
78
|
kind: "permission",
|
|
72
79
|
key: `permission ${request.requestID}`,
|
|
73
80
|
session,
|
|
74
|
-
request
|
|
81
|
+
request,
|
|
82
|
+
...session.depth > 0 && prompting.has(request.requestID) ? {
|
|
83
|
+
prompted: true
|
|
84
|
+
} : {}
|
|
75
85
|
})), ...asks.questions.filter((question) => question.sessionID === session.sessionID).map((question) => ({
|
|
76
86
|
kind: "question",
|
|
77
87
|
key: `question ${question.requestID}`,
|
|
@@ -84,11 +94,13 @@ function asksOf(asks, forms) {
|
|
|
84
94
|
form
|
|
85
95
|
}))]);
|
|
86
96
|
}
|
|
87
|
-
function pathOf(asks, sessionID, topTitle) {
|
|
97
|
+
function pathOf(asks, sessionID, topTitle, currentTitle = () => {
|
|
98
|
+
return;
|
|
99
|
+
}) {
|
|
88
100
|
const byID = new Map(asks.sessions.map((session) => [session.sessionID, session]));
|
|
89
101
|
const titles = [];
|
|
90
102
|
for (let at = byID.get(sessionID);at && titles.length <= byID.size; at = byID.get(at.parentID))
|
|
91
|
-
titles.unshift(at.title);
|
|
103
|
+
titles.unshift(currentTitle(at.sessionID) || at.title);
|
|
92
104
|
return [topTitle, ...titles].join(" \u203A ");
|
|
93
105
|
}
|
|
94
106
|
var clip = (text, max = 80) => text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
@@ -97,9 +109,13 @@ function askTitle(ask) {
|
|
|
97
109
|
case "permission": {
|
|
98
110
|
const {
|
|
99
111
|
action,
|
|
100
|
-
resources
|
|
112
|
+
resources,
|
|
113
|
+
passesOn
|
|
101
114
|
} = ask.request;
|
|
102
|
-
|
|
115
|
+
const what = resources.length ? `${action} ${resources.join(", ")}` : action;
|
|
116
|
+
if (ask.session.depth > 0)
|
|
117
|
+
return clip(`Permission: ${what}`);
|
|
118
|
+
return clip(passesOn ? `Permission (top session): courier_answer once for "${what}"` : `Permission (top session): ${what}`);
|
|
103
119
|
}
|
|
104
120
|
case "question": {
|
|
105
121
|
const [first, ...rest] = ask.question.questions;
|
|
@@ -112,14 +128,89 @@ function askTitle(ask) {
|
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
130
|
function askFooter(ask) {
|
|
115
|
-
|
|
131
|
+
const top = ask.session.depth === 0;
|
|
132
|
+
return [top ? "top session" : `depth ${ask.session.depth}`, ask.session.group ? `group ${ask.session.group}` : "", ask.kind === "permission" && ask.prompted ? "the top's prompt asks" : "", ask.kind === "question" && ask.question.stopped ? "cut off, answered by message" : ""].filter(Boolean).join(" \xB7 ");
|
|
116
133
|
}
|
|
117
|
-
|
|
118
|
-
function shown(field, answers) {
|
|
119
|
-
if ("hidden" in field && field.hidden)
|
|
120
|
-
return false;
|
|
134
|
+
function active(field, answers) {
|
|
121
135
|
const when = "when" in field ? field.when ?? [] : [];
|
|
122
|
-
return when.every((
|
|
136
|
+
return when.every(({
|
|
137
|
+
key,
|
|
138
|
+
op,
|
|
139
|
+
value
|
|
140
|
+
}) => {
|
|
141
|
+
const answer = answers[key];
|
|
142
|
+
if (answer === undefined)
|
|
143
|
+
return false;
|
|
144
|
+
const hit = Array.isArray(answer) ? answer.some((item) => item === value) : answer === value;
|
|
145
|
+
return op === "eq" ? hit : !hit;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
149
|
+
var isDate = (text) => /^\d{4}-\d{2}-\d{2}$/.test(text) && new Date(`${text}T00:00:00.000Z`).toISOString().slice(0, 10) === text;
|
|
150
|
+
function misfit(field, value) {
|
|
151
|
+
if (field.type === "string") {
|
|
152
|
+
if (typeof value !== "string")
|
|
153
|
+
return "Expected text";
|
|
154
|
+
if (field.required && !value)
|
|
155
|
+
return "Answer required";
|
|
156
|
+
if (field.minLength !== undefined && value.length < field.minLength)
|
|
157
|
+
return `Must be at least ${field.minLength} characters`;
|
|
158
|
+
if (field.maxLength !== undefined && value.length > field.maxLength)
|
|
159
|
+
return `Must be at most ${field.maxLength} characters`;
|
|
160
|
+
if (field.pattern !== undefined) {
|
|
161
|
+
try {
|
|
162
|
+
if (!new RegExp(field.pattern).test(value))
|
|
163
|
+
return `Must match pattern: ${field.pattern}`;
|
|
164
|
+
} catch {
|
|
165
|
+
return `Invalid pattern: ${field.pattern}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (field.format === "email" && !EMAIL.test(value))
|
|
169
|
+
return "Expected an email address";
|
|
170
|
+
if (field.format === "uri" && !URL.canParse(value))
|
|
171
|
+
return "Expected a URL";
|
|
172
|
+
if (field.format === "date" && !isDate(value))
|
|
173
|
+
return "Expected a date (YYYY-MM-DD)";
|
|
174
|
+
if (field.format === "date-time" && Number.isNaN(new Date(value).getTime()))
|
|
175
|
+
return "Expected a date and time";
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (field.type === "number" || field.type === "integer") {
|
|
179
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
180
|
+
return "Expected a number";
|
|
181
|
+
if (field.type === "integer" && !Number.isInteger(value))
|
|
182
|
+
return "Expected an integer";
|
|
183
|
+
if (typeof field.minimum === "number" && value < field.minimum)
|
|
184
|
+
return `Must be at least ${field.minimum}`;
|
|
185
|
+
if (typeof field.maximum === "number" && value > field.maximum)
|
|
186
|
+
return `Must be at most ${field.maximum}`;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (field.type === "multiselect") {
|
|
190
|
+
if (!Array.isArray(value))
|
|
191
|
+
return "Expected selections";
|
|
192
|
+
if (field.required && !value.length)
|
|
193
|
+
return "Select at least one option";
|
|
194
|
+
if (field.minItems !== undefined && value.length < field.minItems)
|
|
195
|
+
return `Select at least ${field.minItems}`;
|
|
196
|
+
if (field.maxItems !== undefined && value.length > field.maxItems)
|
|
197
|
+
return `Select at most ${field.maxItems}`;
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
var numberOf = (limit) => typeof limit === "number" ? limit : undefined;
|
|
202
|
+
function limitsOf(field) {
|
|
203
|
+
const range = (low, high, unit = "") => {
|
|
204
|
+
if (low !== undefined && high !== undefined)
|
|
205
|
+
return `${low}-${high}${unit}`;
|
|
206
|
+
if (low !== undefined)
|
|
207
|
+
return `at least ${low}${unit}`;
|
|
208
|
+
if (high !== undefined)
|
|
209
|
+
return `at most ${high}${unit}`;
|
|
210
|
+
return;
|
|
211
|
+
};
|
|
212
|
+
const parts = field.type === "number" || field.type === "integer" ? [range(numberOf(field.minimum), numberOf(field.maximum))] : field.type === "string" ? [range(field.minLength, field.maxLength, " characters"), field.format, field.pattern ? `/${field.pattern}/` : undefined] : [];
|
|
213
|
+
return parts.filter(Boolean).join(", ") || undefined;
|
|
123
214
|
}
|
|
124
215
|
function chooseOptions(choices, picked, {
|
|
125
216
|
multiple = false,
|
|
@@ -163,8 +254,10 @@ function toggle(picked, value) {
|
|
|
163
254
|
var OWN = Symbol("own");
|
|
164
255
|
var DONE = Symbol("done");
|
|
165
256
|
var SKIP = Symbol("skip");
|
|
166
|
-
|
|
257
|
+
var NOTE = Symbol("note");
|
|
258
|
+
function addAsksInbox(ctx, follows) {
|
|
167
259
|
const person = ctx.client.rpc(personRpc);
|
|
260
|
+
const currentTitle = (sessionID) => ctx.data.session.get(sessionID)?.title;
|
|
168
261
|
const location = (sessionID) => ctx.data.session.get(sessionID)?.location ?? ctx.location ?? ctx.data.location.default();
|
|
169
262
|
const openSession = () => {
|
|
170
263
|
const route = ctx.ui.router.current();
|
|
@@ -186,31 +279,46 @@ function addAsksInbox(ctx) {
|
|
|
186
279
|
for (const ask of listed)
|
|
187
280
|
if (!firstSeen.has(ask.key))
|
|
188
281
|
firstSeen.set(ask.key, seen++);
|
|
189
|
-
|
|
282
|
+
const rank = (ask) => ask.session.depth === 0 ? 0 : 1;
|
|
283
|
+
listed.sort((a, b) => rank(a) - rank(b) || firstSeen.get(a.key) - firstSeen.get(b.key));
|
|
190
284
|
return {
|
|
191
285
|
asks,
|
|
192
286
|
listed
|
|
193
287
|
};
|
|
194
288
|
}
|
|
289
|
+
const reason = (error) => {
|
|
290
|
+
if (error instanceof Error)
|
|
291
|
+
return error.message;
|
|
292
|
+
return typeof error === "object" && error && "message" in error ? String(error.message) : String(error);
|
|
293
|
+
};
|
|
195
294
|
const fail = (what) => (error) => ctx.ui.toast.show({
|
|
196
295
|
title: "courier",
|
|
197
|
-
message: `${what}: ${
|
|
296
|
+
message: `${what}: ${reason(error)}`,
|
|
198
297
|
variant: "error"
|
|
199
298
|
});
|
|
200
299
|
async function choose(title, choices, {
|
|
201
300
|
multiple = false,
|
|
202
301
|
own = false,
|
|
203
|
-
optional = false
|
|
302
|
+
optional = false,
|
|
303
|
+
asks = "",
|
|
304
|
+
start = []
|
|
204
305
|
} = {}) {
|
|
205
|
-
const picked = [];
|
|
306
|
+
const picked = [...start];
|
|
206
307
|
for (;; ) {
|
|
207
308
|
const value = await ctx.ui.dialog.select({
|
|
208
309
|
title,
|
|
209
|
-
|
|
310
|
+
...!multiple && start[0] !== undefined ? {
|
|
311
|
+
current: start[0]
|
|
312
|
+
} : {},
|
|
313
|
+
options: [...asks ? [{
|
|
314
|
+
title: asks,
|
|
315
|
+
value: NOTE,
|
|
316
|
+
disabled: true
|
|
317
|
+
}] : [], ...chooseOptions(choices, picked, {
|
|
210
318
|
multiple,
|
|
211
319
|
own,
|
|
212
320
|
optional
|
|
213
|
-
})
|
|
321
|
+
})]
|
|
214
322
|
});
|
|
215
323
|
if (value === undefined)
|
|
216
324
|
return;
|
|
@@ -313,6 +421,9 @@ function addAsksInbox(ctx) {
|
|
|
313
421
|
...field.description ? {
|
|
314
422
|
description: field.description
|
|
315
423
|
} : {},
|
|
424
|
+
...field.placeholder ? {
|
|
425
|
+
placeholder: field.placeholder
|
|
426
|
+
} : {},
|
|
316
427
|
...field.default ? {
|
|
317
428
|
value: field.default
|
|
318
429
|
} : {}
|
|
@@ -324,11 +435,16 @@ function addAsksInbox(ctx) {
|
|
|
324
435
|
}
|
|
325
436
|
}
|
|
326
437
|
async function numeric(title, field, optional) {
|
|
438
|
+
const label = field.title ?? field.key;
|
|
327
439
|
const whole = field.type === "integer";
|
|
328
440
|
for (;; ) {
|
|
329
441
|
const text = await typed(title, {
|
|
330
442
|
...field.description ? {
|
|
331
443
|
description: field.description
|
|
444
|
+
} : {},
|
|
445
|
+
...placeholder(field),
|
|
446
|
+
...field.default !== undefined ? {
|
|
447
|
+
default: String(field.default)
|
|
332
448
|
} : {}
|
|
333
449
|
}, optional);
|
|
334
450
|
if (text === undefined || text === SKIP)
|
|
@@ -338,13 +454,36 @@ function addAsksInbox(ctx) {
|
|
|
338
454
|
return value;
|
|
339
455
|
ctx.ui.toast.show({
|
|
340
456
|
title: "courier",
|
|
341
|
-
message: `${
|
|
457
|
+
message: `${label} takes ${whole ? "a whole number" : "a number"}`,
|
|
342
458
|
variant: "warning"
|
|
343
459
|
});
|
|
344
460
|
}
|
|
345
461
|
}
|
|
346
|
-
|
|
347
|
-
const
|
|
462
|
+
const placeholder = (field) => {
|
|
463
|
+
const limits = limitsOf(field);
|
|
464
|
+
return limits ? {
|
|
465
|
+
placeholder: limits
|
|
466
|
+
} : {};
|
|
467
|
+
};
|
|
468
|
+
async function answerField(form, field) {
|
|
469
|
+
for (;; ) {
|
|
470
|
+
const value = await askField(form, field);
|
|
471
|
+
if (value === undefined || value === SKIP)
|
|
472
|
+
return value;
|
|
473
|
+
const problem = misfit(field, value);
|
|
474
|
+
if (!problem)
|
|
475
|
+
return value;
|
|
476
|
+
ctx.ui.toast.show({
|
|
477
|
+
title: "courier",
|
|
478
|
+
message: `${field.title ?? field.key}: ${problem}`,
|
|
479
|
+
variant: "warning"
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function askField(form, field) {
|
|
484
|
+
const label = field.title ?? field.key;
|
|
485
|
+
const title = form.title ? `${form.title} \xB7 ${label}` : label;
|
|
486
|
+
const asks = field.description ?? "";
|
|
348
487
|
const optional = !(("required" in field) && field.required);
|
|
349
488
|
switch (field.type) {
|
|
350
489
|
case "external":
|
|
@@ -356,7 +495,14 @@ function addAsksInbox(ctx) {
|
|
|
356
495
|
case "boolean":
|
|
357
496
|
return ctx.ui.dialog.select({
|
|
358
497
|
title,
|
|
359
|
-
|
|
498
|
+
...field.default !== undefined ? {
|
|
499
|
+
current: field.default
|
|
500
|
+
} : {},
|
|
501
|
+
options: [...asks ? [{
|
|
502
|
+
title: asks,
|
|
503
|
+
value: NOTE,
|
|
504
|
+
disabled: true
|
|
505
|
+
}] : [], {
|
|
360
506
|
title: "Yes",
|
|
361
507
|
value: true
|
|
362
508
|
}, {
|
|
@@ -367,7 +513,9 @@ function addAsksInbox(ctx) {
|
|
|
367
513
|
case "multiselect": {
|
|
368
514
|
const picked = await choose(title, field.options, {
|
|
369
515
|
multiple: true,
|
|
370
|
-
own: !!field.custom
|
|
516
|
+
own: !!field.custom,
|
|
517
|
+
asks,
|
|
518
|
+
start: field.default ?? []
|
|
371
519
|
});
|
|
372
520
|
if (picked?.length === 0 && optional)
|
|
373
521
|
return SKIP;
|
|
@@ -375,10 +523,15 @@ function addAsksInbox(ctx) {
|
|
|
375
523
|
}
|
|
376
524
|
case "string": {
|
|
377
525
|
if (!field.options?.length)
|
|
378
|
-
return typed(title,
|
|
526
|
+
return typed(title, {
|
|
527
|
+
...field,
|
|
528
|
+
...placeholder(field)
|
|
529
|
+
}, optional);
|
|
379
530
|
const picked = await choose(title, field.options, {
|
|
380
531
|
own: !!field.custom,
|
|
381
|
-
optional
|
|
532
|
+
optional,
|
|
533
|
+
asks,
|
|
534
|
+
start: field.default === undefined ? [] : [field.default]
|
|
382
535
|
});
|
|
383
536
|
return picked && (picked[0] ?? SKIP);
|
|
384
537
|
}
|
|
@@ -393,19 +546,36 @@ function addAsksInbox(ctx) {
|
|
|
393
546
|
} = ask;
|
|
394
547
|
const answer2 = {};
|
|
395
548
|
for (const field of form.fields) {
|
|
396
|
-
if (!
|
|
549
|
+
if (!active(field, answer2))
|
|
397
550
|
continue;
|
|
398
|
-
|
|
551
|
+
if ("hidden" in field && field.hidden) {
|
|
552
|
+
if ("default" in field && field.default !== undefined)
|
|
553
|
+
answer2[field.key] = field.default;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const value = await answerField(form, field);
|
|
399
557
|
if (value === undefined)
|
|
400
558
|
return false;
|
|
401
559
|
if (value !== SKIP)
|
|
402
560
|
answer2[field.key] = value;
|
|
403
561
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
562
|
+
try {
|
|
563
|
+
await ctx.client.session.form.reply({
|
|
564
|
+
sessionID: form.sessionID,
|
|
565
|
+
formID: form.id,
|
|
566
|
+
answer: answer2
|
|
567
|
+
});
|
|
568
|
+
} catch (error) {
|
|
569
|
+
const tag = error?._tag;
|
|
570
|
+
if (tag !== "FormNotFoundError" && tag !== "FormAlreadySettledError")
|
|
571
|
+
throw error;
|
|
572
|
+
ctx.ui.toast.show({
|
|
573
|
+
title: "courier",
|
|
574
|
+
message: "It no longer waits: answered or settled some other way.",
|
|
575
|
+
variant: "warning"
|
|
576
|
+
});
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
409
579
|
return true;
|
|
410
580
|
}
|
|
411
581
|
const answer = (ask) => {
|
|
@@ -418,6 +588,68 @@ function addAsksInbox(ctx) {
|
|
|
418
588
|
return answerForm(ask);
|
|
419
589
|
}
|
|
420
590
|
};
|
|
591
|
+
let listing;
|
|
592
|
+
let following;
|
|
593
|
+
const keysOf = (listed) => listed.map((ask) => ask.key).join(`
|
|
594
|
+
`);
|
|
595
|
+
let reads = 0;
|
|
596
|
+
const follow = async () => {
|
|
597
|
+
const sessionID = listing?.sessionID;
|
|
598
|
+
if (!sessionID)
|
|
599
|
+
return;
|
|
600
|
+
const ticket = ++reads;
|
|
601
|
+
const found = await read(sessionID);
|
|
602
|
+
const shown = listing;
|
|
603
|
+
if (ticket === reads && shown?.sessionID === sessionID && keysOf(found.listed) !== shown.keys)
|
|
604
|
+
shown.replace(found);
|
|
605
|
+
};
|
|
606
|
+
async function showList(sessionID, {
|
|
607
|
+
asks,
|
|
608
|
+
listed
|
|
609
|
+
}) {
|
|
610
|
+
let replace = () => {
|
|
611
|
+
return;
|
|
612
|
+
};
|
|
613
|
+
const newer = new Promise((resolve) => {
|
|
614
|
+
replace = resolve;
|
|
615
|
+
});
|
|
616
|
+
listing = {
|
|
617
|
+
sessionID,
|
|
618
|
+
keys: keysOf(listed),
|
|
619
|
+
replace
|
|
620
|
+
};
|
|
621
|
+
const top = ctx.data.session.get(asks.top)?.title ?? asks.top;
|
|
622
|
+
const choosing = ctx.ui.dialog.select({
|
|
623
|
+
title: `Waiting on you (${listed.length})`,
|
|
624
|
+
placeholder: "Search",
|
|
625
|
+
options: listed.map((ask) => ({
|
|
626
|
+
title: askTitle(ask),
|
|
627
|
+
value: ask,
|
|
628
|
+
description: pathOf(asks, ask.session.sessionID, top, currentTitle),
|
|
629
|
+
footer: askFooter(ask)
|
|
630
|
+
}))
|
|
631
|
+
});
|
|
632
|
+
ctx.ui.dialog.set({
|
|
633
|
+
size: "large"
|
|
634
|
+
});
|
|
635
|
+
try {
|
|
636
|
+
return await Promise.race([choosing.then((chosen) => ({
|
|
637
|
+
chosen
|
|
638
|
+
})), newer.then((read2) => ({
|
|
639
|
+
read: read2
|
|
640
|
+
}))]);
|
|
641
|
+
} finally {
|
|
642
|
+
listing = undefined;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function answerChosen(chosen) {
|
|
646
|
+
const done = await answer(chosen).catch((error) => {
|
|
647
|
+
fail("The answer was not passed on")(error);
|
|
648
|
+
return false;
|
|
649
|
+
});
|
|
650
|
+
if (done)
|
|
651
|
+
answered.add(chosen.key);
|
|
652
|
+
}
|
|
421
653
|
let opened = false;
|
|
422
654
|
async function open(sessionID = openSession()) {
|
|
423
655
|
if (!sessionID) {
|
|
@@ -432,83 +664,95 @@ function addAsksInbox(ctx) {
|
|
|
432
664
|
return;
|
|
433
665
|
opened = true;
|
|
434
666
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
ctx.ui.toast.show({
|
|
446
|
-
title: "courier",
|
|
447
|
-
message: "Nothing in this courier tree waits on you.",
|
|
448
|
-
variant: "info"
|
|
449
|
-
});
|
|
450
|
-
return;
|
|
667
|
+
let found = await read(sessionID).catch(fail("The asks could not be read"));
|
|
668
|
+
let first = true;
|
|
669
|
+
let shown = false;
|
|
670
|
+
while (found?.listed.length) {
|
|
671
|
+
first = false;
|
|
672
|
+
const next = await showList(sessionID, found);
|
|
673
|
+
shown = "read" in next;
|
|
674
|
+
if ("read" in next) {
|
|
675
|
+
found = next.read;
|
|
676
|
+
continue;
|
|
451
677
|
}
|
|
452
|
-
|
|
453
|
-
const choosing = ctx.ui.dialog.select({
|
|
454
|
-
title: `Waiting on you (${listed.length})`,
|
|
455
|
-
placeholder: "Search",
|
|
456
|
-
options: listed.map((ask) => ({
|
|
457
|
-
title: askTitle(ask),
|
|
458
|
-
value: ask,
|
|
459
|
-
description: pathOf(asks, ask.session.sessionID, top),
|
|
460
|
-
footer: askFooter(ask)
|
|
461
|
-
}))
|
|
462
|
-
});
|
|
463
|
-
ctx.ui.dialog.set({
|
|
464
|
-
size: "large"
|
|
465
|
-
});
|
|
466
|
-
const chosen = await choosing;
|
|
467
|
-
if (!chosen)
|
|
678
|
+
if (!next.chosen)
|
|
468
679
|
return;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return false;
|
|
472
|
-
});
|
|
473
|
-
if (done)
|
|
474
|
-
answered.add(chosen.key);
|
|
680
|
+
await answerChosen(next.chosen);
|
|
681
|
+
found = await read(sessionID).catch(fail("The asks could not be read"));
|
|
475
682
|
}
|
|
683
|
+
if (!found)
|
|
684
|
+
return;
|
|
685
|
+
if (first)
|
|
686
|
+
ctx.ui.toast.show({
|
|
687
|
+
title: "courier",
|
|
688
|
+
message: "Nothing in this courier tree waits on you.",
|
|
689
|
+
variant: "info"
|
|
690
|
+
});
|
|
691
|
+
if (shown)
|
|
692
|
+
ctx.ui.dialog.clear();
|
|
476
693
|
} finally {
|
|
477
694
|
opened = false;
|
|
695
|
+
clearTimeout(following);
|
|
696
|
+
following = undefined;
|
|
478
697
|
}
|
|
479
698
|
}
|
|
480
699
|
const sounded = new Set;
|
|
700
|
+
const readTops = new Set;
|
|
701
|
+
let named = new Set;
|
|
481
702
|
let settling;
|
|
482
703
|
const sound = async () => {
|
|
483
704
|
const sessionID = openSession();
|
|
484
|
-
if (!sessionID)
|
|
705
|
+
if (!sessionID) {
|
|
706
|
+
named.clear();
|
|
485
707
|
return;
|
|
708
|
+
}
|
|
486
709
|
const {
|
|
487
710
|
asks,
|
|
488
711
|
listed
|
|
489
712
|
} = await read(sessionID);
|
|
713
|
+
const eventSessions = named;
|
|
714
|
+
named = new Set;
|
|
490
715
|
const top = ctx.data.session.get(asks.top)?.title ?? asks.top;
|
|
716
|
+
const first = !readTops.has(asks.top);
|
|
717
|
+
readTops.add(asks.top);
|
|
491
718
|
for (const ask of listed) {
|
|
492
719
|
if (sounded.has(ask.key))
|
|
493
720
|
continue;
|
|
494
721
|
sounded.add(ask.key);
|
|
722
|
+
if (first && !eventSessions.has(ask.session.sessionID))
|
|
723
|
+
continue;
|
|
724
|
+
if (!ctx.data.session.get(ask.session.sessionID)?.parentID)
|
|
725
|
+
continue;
|
|
495
726
|
ctx.attention.notify({
|
|
496
|
-
title: pathOf(asks, ask.session.sessionID, top),
|
|
727
|
+
title: pathOf(asks, ask.session.sessionID, top, currentTitle),
|
|
497
728
|
message: askTitle(ask),
|
|
498
729
|
notification: {
|
|
499
730
|
when: "blurred"
|
|
500
731
|
},
|
|
501
|
-
sound:
|
|
502
|
-
name: soundOf(ask),
|
|
503
|
-
when: "always"
|
|
504
|
-
}
|
|
732
|
+
sound: false
|
|
505
733
|
});
|
|
506
734
|
}
|
|
507
735
|
};
|
|
736
|
+
sound().catch(() => {
|
|
737
|
+
return;
|
|
738
|
+
});
|
|
508
739
|
const unlisten = ctx.data.listen(({
|
|
509
740
|
details
|
|
510
741
|
}) => {
|
|
511
|
-
if (
|
|
742
|
+
if (listing && follows.has(details.type) && !following)
|
|
743
|
+
following = setTimeout(() => {
|
|
744
|
+
following = undefined;
|
|
745
|
+
follow().catch(() => {
|
|
746
|
+
return;
|
|
747
|
+
});
|
|
748
|
+
}, SETTLE_MS);
|
|
749
|
+
if (details.type !== "permission.asked" && details.type !== "form.created")
|
|
750
|
+
return;
|
|
751
|
+
const data = details.data;
|
|
752
|
+
const eventSession = data?.sessionID ?? data?.form?.sessionID;
|
|
753
|
+
if (eventSession)
|
|
754
|
+
named.add(eventSession);
|
|
755
|
+
if (settling)
|
|
512
756
|
return;
|
|
513
757
|
settling = setTimeout(() => {
|
|
514
758
|
settling = undefined;
|
|
@@ -537,6 +781,7 @@ function addAsksInbox(ctx) {
|
|
|
537
781
|
open,
|
|
538
782
|
cleanup: () => {
|
|
539
783
|
clearTimeout(settling);
|
|
784
|
+
clearTimeout(following);
|
|
540
785
|
unlisten();
|
|
541
786
|
}
|
|
542
787
|
};
|
|
@@ -552,7 +797,7 @@ import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
|
552
797
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
553
798
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
554
799
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
555
|
-
import { createEffect, createResource, createRoot, createSignal, For, on, onCleanup, onMount, Show, untrack } from "solid-js";
|
|
800
|
+
import { createEffect, createMemo, createResource, createRoot, createSignal, For, on, onCleanup, onMount, Show, untrack } from "solid-js";
|
|
556
801
|
|
|
557
802
|
// src/tree/rpc.ts
|
|
558
803
|
import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
|
|
@@ -563,12 +808,12 @@ var Summary = Schema2.Struct({
|
|
|
563
808
|
running: Count2,
|
|
564
809
|
waiting: Count2,
|
|
565
810
|
idle: Count2,
|
|
566
|
-
groups: Schema2.Array(Group)
|
|
567
|
-
truncated: Schema2.optionalKey(Count2)
|
|
811
|
+
groups: Schema2.Array(Group)
|
|
568
812
|
});
|
|
569
813
|
var Wait = Schema2.Union([
|
|
570
814
|
Schema2.Struct({ type: Schema2.Literal("permission"), action: Schema2.String, resources: Schema2.Array(Schema2.String) }),
|
|
571
|
-
Schema2.Struct({ type: Schema2.Literal("question"), question: Schema2.String })
|
|
815
|
+
Schema2.Struct({ type: Schema2.Literal("question"), question: Schema2.String }),
|
|
816
|
+
Schema2.Struct({ type: Schema2.Literal("form"), title: Schema2.String })
|
|
572
817
|
]);
|
|
573
818
|
var Row = Schema2.Struct({
|
|
574
819
|
sessionID: Schema2.String,
|
|
@@ -577,6 +822,7 @@ var Row = Schema2.Struct({
|
|
|
577
822
|
depth: Count2,
|
|
578
823
|
state: Schema2.Literals(["running", "waiting", "idle", "gone"]),
|
|
579
824
|
outcome: Schema2.optionalKey(Schema2.Literals(["succeeded", "failed", "interrupted"])),
|
|
825
|
+
stopped: Schema2.optionalKey(Schema2.Boolean),
|
|
580
826
|
report: Schema2.optionalKey(Schema2.Struct({ state: Schema2.Literals(["owed", "reported", "ended"]), status: Schema2.optionalKey(Schema2.String), progressed: Schema2.Boolean })),
|
|
581
827
|
group: Schema2.optionalKey(Schema2.Struct({ name: Schema2.String, standing: Schema2.String })),
|
|
582
828
|
isolated: Schema2.Boolean,
|
|
@@ -594,6 +840,10 @@ var Stopped = Schema2.Struct({
|
|
|
594
840
|
stopped: Schema2.Array(Schema2.Struct({ sessionID: Schema2.String, title: Schema2.String, outcome: Schema2.String })),
|
|
595
841
|
cancelled: Count2
|
|
596
842
|
});
|
|
843
|
+
var Forgotten = Schema2.Union([
|
|
844
|
+
Schema2.Struct({ outcome: Schema2.Literals(["forgotten", "removed", "gone"]) }),
|
|
845
|
+
Schema2.Struct({ outcome: Schema2.Literal("kept"), reason: Schema2.String })
|
|
846
|
+
]);
|
|
597
847
|
var treeRpc = Rpc2.define({
|
|
598
848
|
id: "courier",
|
|
599
849
|
methods: {
|
|
@@ -609,6 +859,11 @@ var treeRpc = Rpc2.define({
|
|
|
609
859
|
input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
|
|
610
860
|
output: Schema2.toStandardSchemaV1(Stopped),
|
|
611
861
|
errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
|
|
862
|
+
},
|
|
863
|
+
cleanup: {
|
|
864
|
+
input: Schema2.toStandardSchemaV1(Schema2.Struct({ sessionID: Schema2.String, by: Schema2.String })),
|
|
865
|
+
output: Schema2.toStandardSchemaV1(Forgotten),
|
|
866
|
+
errors: { refused: Schema2.toStandardSchemaV1(Schema2.Struct({})) }
|
|
612
867
|
}
|
|
613
868
|
},
|
|
614
869
|
events: {}
|
|
@@ -621,9 +876,19 @@ var SETTLE_MS2 = 250;
|
|
|
621
876
|
var PANEL_SETTLE_MS = 1000;
|
|
622
877
|
var PANEL = "courier-tree";
|
|
623
878
|
var TICK_MS = 1000;
|
|
624
|
-
function summaryLine(summary) {
|
|
625
|
-
const
|
|
626
|
-
|
|
879
|
+
function summaryLine(summary, maxWidth = Infinity) {
|
|
880
|
+
const groups = summary.groups.map((group) => `group ${group.name} ${group.reported}/${group.members}`);
|
|
881
|
+
const parts = [summary.waiting ? `${summary.waiting} waiting on you` : "", summary.running ? `${summary.running} running` : "", ...groups, summary.idle ? `${summary.idle} idle` : ""].filter(Boolean);
|
|
882
|
+
if (!parts.length)
|
|
883
|
+
return "";
|
|
884
|
+
const line = (kept) => {
|
|
885
|
+
const dropped = groups.filter((group) => parts.indexOf(group) >= kept).length;
|
|
886
|
+
return ["courier", parts.slice(0, kept).join(" \xB7 "), dropped ? `\xB7 +${counted(dropped, "group")}` : ""].filter(Boolean).join(" ");
|
|
887
|
+
};
|
|
888
|
+
for (let kept = parts.length;kept > 0; kept--)
|
|
889
|
+
if (line(kept).length <= maxWidth)
|
|
890
|
+
return line(kept);
|
|
891
|
+
return parts[0];
|
|
627
892
|
}
|
|
628
893
|
function duration(ms) {
|
|
629
894
|
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
@@ -649,25 +914,29 @@ function bar({
|
|
|
649
914
|
return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${reported}/${started} reported`;
|
|
650
915
|
}
|
|
651
916
|
function waitLine(wait) {
|
|
652
|
-
|
|
917
|
+
if (wait.type === "permission")
|
|
918
|
+
return `asks permission: ${[wait.action, ...wait.resources].join(" ")}`;
|
|
919
|
+
return wait.type === "form" ? `shows a form: ${wait.title}` : `asks: ${wait.question}`;
|
|
653
920
|
}
|
|
654
921
|
function statusLine(row) {
|
|
655
|
-
return [stateWord(row), reportWords(row.report), row.group ? `group ${row.group.name} ${row.group.standing}` : "", row.isolated ? "isolated" : ""].filter(Boolean).join(" \xB7 ");
|
|
922
|
+
return [stateWord(row), reportWords(row.report, row.stopped), row.group ? `group ${row.group.name} ${row.group.standing}` : "", row.isolated ? "isolated" : ""].filter(Boolean).join(" \xB7 ");
|
|
656
923
|
}
|
|
657
924
|
function stateWord(row) {
|
|
658
925
|
if (row.state === "waiting")
|
|
659
926
|
return "waiting on you";
|
|
927
|
+
if (row.state === "idle" && row.stopped)
|
|
928
|
+
return "stopped";
|
|
660
929
|
if (row.state === "idle" && (row.outcome === "failed" || row.outcome === "interrupted"))
|
|
661
930
|
return row.outcome;
|
|
662
931
|
return row.state;
|
|
663
932
|
}
|
|
664
|
-
function reportWords(report) {
|
|
933
|
+
function reportWords(report, stopped) {
|
|
665
934
|
if (!report)
|
|
666
935
|
return "";
|
|
667
936
|
if (report.state === "owed")
|
|
668
937
|
return "owes a report";
|
|
669
938
|
if (report.state === "ended")
|
|
670
|
-
return "ended without a report";
|
|
939
|
+
return stopped ? "" : "ended without a report";
|
|
671
940
|
return ["reported", report.status].filter(Boolean).join(" ");
|
|
672
941
|
}
|
|
673
942
|
var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
@@ -733,34 +1002,44 @@ function addTreeStatus(ctx, onWaiting) {
|
|
|
733
1002
|
location: location(sessionID)
|
|
734
1003
|
}).then((summary) => ({
|
|
735
1004
|
sessionID,
|
|
736
|
-
|
|
1005
|
+
summary,
|
|
737
1006
|
waiting: summary.waiting
|
|
738
1007
|
}), () => ({
|
|
739
1008
|
sessionID,
|
|
740
|
-
|
|
1009
|
+
summary: undefined,
|
|
741
1010
|
waiting: 0
|
|
742
1011
|
})));
|
|
743
|
-
const
|
|
1012
|
+
const shown = () => {
|
|
744
1013
|
const latest = read.state === "ready" || read.state === "refreshing" ? read.latest : undefined;
|
|
745
1014
|
return latest && latest.sessionID === input.sessionID ? latest : undefined;
|
|
746
1015
|
};
|
|
747
|
-
const
|
|
1016
|
+
const [width, setWidth] = createSignal(0);
|
|
1017
|
+
const present = () => shown()?.summary ? summaryLine(shown().summary) : "";
|
|
1018
|
+
const line = () => width() && shown()?.summary ? summaryLine(shown().summary, width() - 1) : "";
|
|
748
1019
|
const click = () => {
|
|
749
|
-
const latest =
|
|
1020
|
+
const latest = shown();
|
|
750
1021
|
if (latest?.waiting)
|
|
751
1022
|
onWaiting?.(latest.sessionID);
|
|
752
1023
|
};
|
|
753
1024
|
return _$createComponent(Show, {
|
|
754
1025
|
get when() {
|
|
755
|
-
return
|
|
1026
|
+
return present();
|
|
756
1027
|
},
|
|
757
1028
|
get children() {
|
|
758
|
-
var _el$ = _$createElement("
|
|
1029
|
+
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createTextNode(` `);
|
|
759
1030
|
_$insertNode(_el$, _el$2);
|
|
760
|
-
_$setProp(_el$, "
|
|
761
|
-
_$setProp(_el$, "
|
|
762
|
-
_$
|
|
763
|
-
_$
|
|
1031
|
+
_$setProp(_el$, "flexGrow", 1);
|
|
1032
|
+
_$setProp(_el$, "flexShrink", 1);
|
|
1033
|
+
_$setProp(_el$, "minWidth", 0);
|
|
1034
|
+
_$setProp(_el$, "onSizeChange", function() {
|
|
1035
|
+
if (this.width > 0)
|
|
1036
|
+
setWidth(this.width);
|
|
1037
|
+
});
|
|
1038
|
+
_$insertNode(_el$2, _el$3);
|
|
1039
|
+
_$setProp(_el$2, "wrapMode", "none");
|
|
1040
|
+
_$setProp(_el$2, "onMouseUp", click);
|
|
1041
|
+
_$insert(_el$2, line, null);
|
|
1042
|
+
_$effect((_$p) => _$setProp(_el$2, "fg", ctx.theme.text.muted, _$p));
|
|
764
1043
|
return _el$;
|
|
765
1044
|
}
|
|
766
1045
|
});
|
|
@@ -815,7 +1094,52 @@ async function confirmStop(ctx, courier, location, by, target) {
|
|
|
815
1094
|
}
|
|
816
1095
|
return true;
|
|
817
1096
|
}
|
|
1097
|
+
function forgottenLine(title, result) {
|
|
1098
|
+
const name = `"${oneLine(title)}"`;
|
|
1099
|
+
if (result.outcome === "kept")
|
|
1100
|
+
return `Kept ${name} and its worktree: ${result.reason}`;
|
|
1101
|
+
if (result.outcome === "removed")
|
|
1102
|
+
return `Forgot ${name} and removed its worktree`;
|
|
1103
|
+
return result.outcome === "gone" ? `Forgot ${name}; its worktree was already gone` : `Forgot ${name}`;
|
|
1104
|
+
}
|
|
1105
|
+
async function confirmForget(ctx, courier, location, by, target, done) {
|
|
1106
|
+
const what = target.isolated ? "Its worktree is removed unless it has uncommitted changes or commits found nowhere else." : "It leaves this tree and courier_children.";
|
|
1107
|
+
const confirmed = await ctx.ui.dialog.confirm({
|
|
1108
|
+
title: "Forget session",
|
|
1109
|
+
message: `Forget "${oneLine(target.title)}" (${target.sessionID})? ${what}`,
|
|
1110
|
+
label: {
|
|
1111
|
+
confirm: "Forget",
|
|
1112
|
+
cancel: "Keep"
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
if (!confirmed)
|
|
1116
|
+
return false;
|
|
1117
|
+
try {
|
|
1118
|
+
const result = await courier.cleanup({
|
|
1119
|
+
sessionID: target.sessionID,
|
|
1120
|
+
by
|
|
1121
|
+
}, {
|
|
1122
|
+
location: location(by)
|
|
1123
|
+
});
|
|
1124
|
+
ctx.ui.toast.show({
|
|
1125
|
+
title: "courier",
|
|
1126
|
+
message: forgottenLine(target.title, result),
|
|
1127
|
+
variant: result.outcome === "kept" ? "warning" : "success"
|
|
1128
|
+
});
|
|
1129
|
+
if (result.outcome !== "kept")
|
|
1130
|
+
done();
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
ctx.ui.toast.show({
|
|
1133
|
+
title: "courier forget",
|
|
1134
|
+
message: reason(error),
|
|
1135
|
+
variant: "error"
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
818
1140
|
var FOCUS_PROMPT = "pane.focus.left";
|
|
1141
|
+
var SPLIT_WIDTH = 80;
|
|
1142
|
+
var oneLine = (title) => title.replace(/[\r\n]+/g, " ");
|
|
819
1143
|
function TreePanel(props) {
|
|
820
1144
|
const {
|
|
821
1145
|
ctx,
|
|
@@ -855,10 +1179,18 @@ function TreePanel(props) {
|
|
|
855
1179
|
tree
|
|
856
1180
|
});
|
|
857
1181
|
});
|
|
858
|
-
createEffect(() =>
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1182
|
+
createEffect(() => {
|
|
1183
|
+
const {
|
|
1184
|
+
presentation,
|
|
1185
|
+
focused
|
|
1186
|
+
} = input;
|
|
1187
|
+
view.note(ctx.renderer.width > SPLIT_WIDTH ? {
|
|
1188
|
+
presentation,
|
|
1189
|
+
focused
|
|
1190
|
+
} : {
|
|
1191
|
+
focused
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
862
1194
|
onMount(() => {
|
|
863
1195
|
if (!view.refocus)
|
|
864
1196
|
return;
|
|
@@ -868,23 +1200,39 @@ function TreePanel(props) {
|
|
|
868
1200
|
const [now, setNow] = createSignal(Date.now());
|
|
869
1201
|
const tick = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
870
1202
|
onCleanup(() => clearInterval(tick));
|
|
871
|
-
const [
|
|
1203
|
+
const [selectedID, setSelectedID] = createSignal();
|
|
872
1204
|
let placed = false;
|
|
873
1205
|
createEffect(() => {
|
|
874
|
-
const
|
|
875
|
-
if (
|
|
1206
|
+
const found = placed ? undefined : rows().find((row) => row.sessionID === input.sessionID);
|
|
1207
|
+
if (!found)
|
|
876
1208
|
return;
|
|
877
1209
|
placed = true;
|
|
878
|
-
|
|
1210
|
+
setSelectedID(found.sessionID);
|
|
879
1211
|
});
|
|
880
|
-
|
|
1212
|
+
let at = 0;
|
|
1213
|
+
const selected = createMemo(() => {
|
|
1214
|
+
const id = selectedID();
|
|
1215
|
+
const found = rows().findIndex((row) => row.sessionID === id);
|
|
1216
|
+
const index = found >= 0 ? found : Math.min(at, rows().length - 1);
|
|
1217
|
+
at = Math.max(index, 0);
|
|
1218
|
+
return index;
|
|
1219
|
+
});
|
|
1220
|
+
createEffect(() => {
|
|
1221
|
+
const row = rows()[selected()];
|
|
1222
|
+
if (row && row.sessionID !== selectedID())
|
|
1223
|
+
setSelectedID(row.sessionID);
|
|
1224
|
+
});
|
|
1225
|
+
const current = () => rows()[selected()];
|
|
881
1226
|
let scroll;
|
|
882
1227
|
createEffect(() => {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
scroll?.scrollChildIntoView(`courier-row-${index}`);
|
|
1228
|
+
if (selected() >= 0)
|
|
1229
|
+
scroll?.scrollChildIntoView(`courier-row-${selected()}`);
|
|
886
1230
|
});
|
|
887
|
-
const move = (by) =>
|
|
1231
|
+
const move = (by) => {
|
|
1232
|
+
const next = rows()[Math.max(0, Math.min(rows().length - 1, selected() + by))];
|
|
1233
|
+
if (next)
|
|
1234
|
+
setSelectedID(next.sessionID);
|
|
1235
|
+
};
|
|
888
1236
|
const open = () => {
|
|
889
1237
|
const row = current();
|
|
890
1238
|
if (row)
|
|
@@ -902,6 +1250,22 @@ function TreePanel(props) {
|
|
|
902
1250
|
under: reach(rows(), row.sessionID) - 1
|
|
903
1251
|
});
|
|
904
1252
|
};
|
|
1253
|
+
const forgetSelected = () => {
|
|
1254
|
+
const row = current();
|
|
1255
|
+
if (!row)
|
|
1256
|
+
return;
|
|
1257
|
+
if (row.state !== "gone")
|
|
1258
|
+
return ctx.ui.toast.show({
|
|
1259
|
+
title: "courier",
|
|
1260
|
+
message: `x forgets only a gone session; "${oneLine(row.title)}" is ${row.state}.`,
|
|
1261
|
+
variant: "warning"
|
|
1262
|
+
});
|
|
1263
|
+
confirmForget(ctx, courier, location, view.root, {
|
|
1264
|
+
sessionID: row.sessionID,
|
|
1265
|
+
title: row.title,
|
|
1266
|
+
isolated: row.isolated
|
|
1267
|
+
}, () => void refetch());
|
|
1268
|
+
};
|
|
905
1269
|
ctx.keymap.layer(() => ({
|
|
906
1270
|
priority: 10,
|
|
907
1271
|
enabled: () => input.focused,
|
|
@@ -935,6 +1299,11 @@ function TreePanel(props) {
|
|
|
935
1299
|
title: "Stop session and those under it",
|
|
936
1300
|
group: "Courier",
|
|
937
1301
|
run: stopSelected
|
|
1302
|
+
}, {
|
|
1303
|
+
bind: "x",
|
|
1304
|
+
title: "Forget gone session",
|
|
1305
|
+
group: "Courier",
|
|
1306
|
+
run: forgetSelected
|
|
938
1307
|
}, {
|
|
939
1308
|
bind: "r",
|
|
940
1309
|
title: "Refresh",
|
|
@@ -959,47 +1328,46 @@ function TreePanel(props) {
|
|
|
959
1328
|
return row.state === "gone" ? theme.text.muted : theme.text.base;
|
|
960
1329
|
};
|
|
961
1330
|
return (() => {
|
|
962
|
-
var _el$
|
|
963
|
-
_$insertNode(_el$3, _el$4);
|
|
964
|
-
_$insertNode(_el$3, _el$13);
|
|
965
|
-
_$insertNode(_el$3, _el$14);
|
|
966
|
-
_$setProp(_el$3, "flexDirection", "column");
|
|
967
|
-
_$setProp(_el$3, "flexGrow", 1);
|
|
968
|
-
_$setProp(_el$3, "paddingLeft", 1);
|
|
969
|
-
_$setProp(_el$3, "paddingRight", 1);
|
|
1331
|
+
var _el$4 = _$createElement("box"), _el$5 = _$createElement("box"), _el$6 = _$createElement("text"), _el$13 = _$createElement("scrollbox"), _el$14 = _$createElement("text");
|
|
970
1332
|
_$insertNode(_el$4, _el$5);
|
|
971
|
-
_$
|
|
972
|
-
_$
|
|
973
|
-
_$setProp(_el$4, "
|
|
974
|
-
_$
|
|
975
|
-
_$setProp(_el$
|
|
976
|
-
_$
|
|
1333
|
+
_$insertNode(_el$4, _el$13);
|
|
1334
|
+
_$insertNode(_el$4, _el$14);
|
|
1335
|
+
_$setProp(_el$4, "flexDirection", "column");
|
|
1336
|
+
_$setProp(_el$4, "flexGrow", 1);
|
|
1337
|
+
_$setProp(_el$4, "paddingLeft", 1);
|
|
1338
|
+
_$setProp(_el$4, "paddingRight", 1);
|
|
1339
|
+
_$insertNode(_el$5, _el$6);
|
|
1340
|
+
_$setProp(_el$5, "flexDirection", "row");
|
|
1341
|
+
_$setProp(_el$5, "flexShrink", 0);
|
|
1342
|
+
_$setProp(_el$5, "gap", 2);
|
|
1343
|
+
_$insertNode(_el$6, _$createTextNode(`courier tree`));
|
|
1344
|
+
_$setProp(_el$6, "wrapMode", "none");
|
|
1345
|
+
_$insert(_el$5, _$createComponent(Show, {
|
|
977
1346
|
get when() {
|
|
978
1347
|
return root().started;
|
|
979
1348
|
},
|
|
980
1349
|
get children() {
|
|
981
|
-
var _el$
|
|
982
|
-
_$setProp(_el$
|
|
983
|
-
_$insert(_el$
|
|
984
|
-
_$effect((_$p) => _$setProp(_el$
|
|
985
|
-
return _el$
|
|
1350
|
+
var _el$8 = _$createElement("text");
|
|
1351
|
+
_$setProp(_el$8, "wrapMode", "none");
|
|
1352
|
+
_$insert(_el$8, () => bar(root()));
|
|
1353
|
+
_$effect((_$p) => _$setProp(_el$8, "fg", theme.text.muted, _$p));
|
|
1354
|
+
return _el$8;
|
|
986
1355
|
}
|
|
987
1356
|
}), null);
|
|
988
|
-
_$insert(_el$
|
|
1357
|
+
_$insert(_el$5, _$createComponent(Show, {
|
|
989
1358
|
get when() {
|
|
990
1359
|
return latest()?.tree?.truncated;
|
|
991
1360
|
},
|
|
992
1361
|
get children() {
|
|
993
|
-
var _el$
|
|
994
|
-
_$insertNode(_el$
|
|
995
|
-
_$
|
|
996
|
-
_$
|
|
997
|
-
_$
|
|
998
|
-
|
|
999
|
-
return _el$8;
|
|
1362
|
+
var _el$9 = _$createElement("text"), _el$0 = _$createTextNode(` older left out`);
|
|
1363
|
+
_$insertNode(_el$9, _el$0);
|
|
1364
|
+
_$setProp(_el$9, "wrapMode", "none");
|
|
1365
|
+
_$insert(_el$9, () => latest()?.tree?.truncated, _el$0);
|
|
1366
|
+
_$effect((_$p) => _$setProp(_el$9, "fg", theme.text.feedback.warning.base, _$p));
|
|
1367
|
+
return _el$9;
|
|
1000
1368
|
}
|
|
1001
1369
|
}), null);
|
|
1002
|
-
_$insert(_el$
|
|
1370
|
+
_$insert(_el$4, _$createComponent(Show, {
|
|
1003
1371
|
get when() {
|
|
1004
1372
|
return latest()?.error;
|
|
1005
1373
|
},
|
|
@@ -1011,7 +1379,7 @@ function TreePanel(props) {
|
|
|
1011
1379
|
return _el$1;
|
|
1012
1380
|
}
|
|
1013
1381
|
}), _el$13);
|
|
1014
|
-
_$insert(_el$
|
|
1382
|
+
_$insert(_el$4, _$createComponent(Show, {
|
|
1015
1383
|
get when() {
|
|
1016
1384
|
return _$memo(() => !!latest()?.tree)() && !rows().length;
|
|
1017
1385
|
},
|
|
@@ -1029,43 +1397,48 @@ function TreePanel(props) {
|
|
|
1029
1397
|
return rows();
|
|
1030
1398
|
},
|
|
1031
1399
|
children: (row, index) => {
|
|
1032
|
-
const chosen = () => index() ===
|
|
1400
|
+
const chosen = () => index() === selected();
|
|
1033
1401
|
const indent = " ".repeat(row.depth - 1);
|
|
1034
1402
|
return (() => {
|
|
1035
|
-
var _el$16 = _$createElement("box"), _el$17 = _$createElement("
|
|
1403
|
+
var _el$16 = _$createElement("box"), _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$20 = _$createTextNode(` `);
|
|
1036
1404
|
_$insertNode(_el$16, _el$17);
|
|
1037
1405
|
_$setProp(_el$16, "flexDirection", "column");
|
|
1038
1406
|
_$setProp(_el$16, "flexShrink", 0);
|
|
1039
1407
|
_$insertNode(_el$17, _el$18);
|
|
1040
1408
|
_$insertNode(_el$17, _el$19);
|
|
1041
|
-
_$setProp(_el$17, "
|
|
1042
|
-
_$
|
|
1043
|
-
_$
|
|
1044
|
-
_$
|
|
1045
|
-
_$
|
|
1046
|
-
_$insert(_el$
|
|
1409
|
+
_$setProp(_el$17, "flexDirection", "row");
|
|
1410
|
+
_$setProp(_el$17, "flexShrink", 0);
|
|
1411
|
+
_$setProp(_el$18, "wrapMode", "none");
|
|
1412
|
+
_$setProp(_el$18, "flexShrink", 1);
|
|
1413
|
+
_$setProp(_el$18, "minWidth", 0);
|
|
1414
|
+
_$insert(_el$18, indent, null);
|
|
1415
|
+
_$insert(_el$18, () => chosen() ? "\u25B8 " : " ", null);
|
|
1416
|
+
_$insert(_el$18, () => oneLine(row.title), null);
|
|
1417
|
+
_$insert(_el$18, () => row.sessionID === input.sessionID ? " (on screen)" : "", null);
|
|
1418
|
+
_$insertNode(_el$19, _el$20);
|
|
1419
|
+
_$setProp(_el$19, "wrapMode", "none");
|
|
1420
|
+
_$setProp(_el$19, "flexShrink", 0);
|
|
1421
|
+
_$insert(_el$19, () => statusLine(row), null);
|
|
1047
1422
|
_$insert(_el$16, _$createComponent(For, {
|
|
1048
1423
|
get each() {
|
|
1049
1424
|
return detailLines(row, rows(), now());
|
|
1050
1425
|
},
|
|
1051
1426
|
children: (line) => (() => {
|
|
1052
|
-
var _el$
|
|
1053
|
-
_$insertNode(_el$
|
|
1054
|
-
_$setProp(_el$
|
|
1055
|
-
_$insert(_el$
|
|
1056
|
-
_$insert(_el$
|
|
1057
|
-
_$effect((_$p) => _$setProp(_el$
|
|
1058
|
-
return _el$
|
|
1427
|
+
var _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` `);
|
|
1428
|
+
_$insertNode(_el$21, _el$22);
|
|
1429
|
+
_$setProp(_el$21, "wrapMode", "none");
|
|
1430
|
+
_$insert(_el$21, indent, _el$22);
|
|
1431
|
+
_$insert(_el$21, line, null);
|
|
1432
|
+
_$effect((_$p) => _$setProp(_el$21, "fg", theme.text.muted, _$p));
|
|
1433
|
+
return _el$21;
|
|
1059
1434
|
})()
|
|
1060
1435
|
}), null);
|
|
1061
1436
|
_$effect((_p$) => {
|
|
1062
|
-
var _v$3 = `courier-row-${index()}`, _v$4 = chosen() && input.focused ? theme.background.raised.high : undefined, _v$5 = rowColor(row), _v$6 =
|
|
1063
|
-
fg: theme.text.muted
|
|
1064
|
-
};
|
|
1437
|
+
var _v$3 = `courier-row-${index()}`, _v$4 = chosen() && input.focused ? theme.background.raised.high : undefined, _v$5 = rowColor(row), _v$6 = theme.text.muted;
|
|
1065
1438
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$16, "id", _v$3, _p$.e));
|
|
1066
1439
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$16, "backgroundColor", _v$4, _p$.t));
|
|
1067
|
-
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$
|
|
1068
|
-
_v$6 !== _p$.o && (_p$.o = _$setProp(_el$19, "
|
|
1440
|
+
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$18, "fg", _v$5, _p$.a));
|
|
1441
|
+
_v$6 !== _p$.o && (_p$.o = _$setProp(_el$19, "fg", _v$6, _p$.o));
|
|
1069
1442
|
return _p$;
|
|
1070
1443
|
}, {
|
|
1071
1444
|
e: undefined,
|
|
@@ -1077,19 +1450,19 @@ function TreePanel(props) {
|
|
|
1077
1450
|
})();
|
|
1078
1451
|
}
|
|
1079
1452
|
}));
|
|
1080
|
-
_$insertNode(_el$14, _$createTextNode(`\u2191\u2193 select \xB7 enter open \xB7 s stop \xB7 r refresh \xB7 f fullscreen \xB7 q close`));
|
|
1453
|
+
_$insertNode(_el$14, _$createTextNode(`\u2191\u2193 select \xB7 enter open \xB7 s stop \xB7 x forget \xB7 r refresh \xB7 f fullscreen \xB7 q close`));
|
|
1081
1454
|
_$setProp(_el$14, "wrapMode", "none");
|
|
1082
1455
|
_$setProp(_el$14, "flexShrink", 0);
|
|
1083
1456
|
_$effect((_p$) => {
|
|
1084
1457
|
var _v$ = theme.text.base, _v$2 = theme.text.muted;
|
|
1085
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$
|
|
1458
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$, _p$.e));
|
|
1086
1459
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$14, "fg", _v$2, _p$.t));
|
|
1087
1460
|
return _p$;
|
|
1088
1461
|
}, {
|
|
1089
1462
|
e: undefined,
|
|
1090
1463
|
t: undefined
|
|
1091
1464
|
});
|
|
1092
|
-
return _el$
|
|
1465
|
+
return _el$4;
|
|
1093
1466
|
})();
|
|
1094
1467
|
}
|
|
1095
1468
|
var USAGE = "/courier tree opens the session tree; /courier stop [session] stops a session and those under it";
|
|
@@ -1187,11 +1560,11 @@ function addTreePanel(ctx) {
|
|
|
1187
1560
|
createEffect(() => {
|
|
1188
1561
|
const route = ctx.ui.router.current();
|
|
1189
1562
|
const sessionID = route.type === "session" ? route.sessionID : undefined;
|
|
1190
|
-
const
|
|
1563
|
+
const shown = ctx.ui.panel.current();
|
|
1191
1564
|
untrack(() => {
|
|
1192
|
-
if (!following ||
|
|
1565
|
+
if (!following || shown && shown.sessionID === sessionID || !following.at && following.to === sessionID)
|
|
1193
1566
|
return;
|
|
1194
|
-
if (!
|
|
1567
|
+
if (!shown && sessionID && following.at === sessionID)
|
|
1195
1568
|
following = undefined;
|
|
1196
1569
|
else if (!sessionID)
|
|
1197
1570
|
hide(following);
|
|
@@ -1237,11 +1610,29 @@ function addTreePanel(ctx) {
|
|
|
1237
1610
|
message: "Open a session first",
|
|
1238
1611
|
variant: "warning"
|
|
1239
1612
|
});
|
|
1240
|
-
const
|
|
1613
|
+
const read = await courier.tree({
|
|
1241
1614
|
sessionID: route.sessionID
|
|
1242
1615
|
}, {
|
|
1243
1616
|
location: location(route.sessionID)
|
|
1244
|
-
}).then((tree) =>
|
|
1617
|
+
}).then((tree) => ({
|
|
1618
|
+
rows: tree.rows
|
|
1619
|
+
}), (error) => ({
|
|
1620
|
+
rows: [],
|
|
1621
|
+
error
|
|
1622
|
+
}));
|
|
1623
|
+
const rows = read.rows;
|
|
1624
|
+
if (!target && "error" in read)
|
|
1625
|
+
return ctx.ui.toast.show({
|
|
1626
|
+
title: "courier stop",
|
|
1627
|
+
message: reason(read.error),
|
|
1628
|
+
variant: "error"
|
|
1629
|
+
});
|
|
1630
|
+
if (!target && !rows.length)
|
|
1631
|
+
return ctx.ui.toast.show({
|
|
1632
|
+
title: "courier stop",
|
|
1633
|
+
message: "No session started with courier_spawn under this one.",
|
|
1634
|
+
variant: "warning"
|
|
1635
|
+
});
|
|
1245
1636
|
const sessionID = target || await ctx.ui.dialog.select({
|
|
1246
1637
|
title: "Stop which session?",
|
|
1247
1638
|
options: rows.map((row2) => ({
|
|
@@ -1314,6 +1705,8 @@ function addTreeAttention(ctx) {
|
|
|
1314
1705
|
const from = typeof metadata.from === "string" ? metadata.from : "";
|
|
1315
1706
|
if (!group && !status)
|
|
1316
1707
|
return;
|
|
1708
|
+
if (!group && ctx.data.session.get(from)?.parentID !== undefined)
|
|
1709
|
+
return;
|
|
1317
1710
|
notified.add(inboxID);
|
|
1318
1711
|
if (notified.size > NOTIFIED_MAX)
|
|
1319
1712
|
notified.delete(notified.values().next().value);
|
|
@@ -1345,7 +1738,7 @@ function addTreeAttention(ctx) {
|
|
|
1345
1738
|
var tui_default = Plugin.define({
|
|
1346
1739
|
id: "courier.tui",
|
|
1347
1740
|
setup: (ctx) => {
|
|
1348
|
-
const inbox = addAsksInbox(ctx);
|
|
1741
|
+
const inbox = addAsksInbox(ctx, CHANGES);
|
|
1349
1742
|
const cleanups = [inbox.cleanup, addTreeStatus(ctx, (sessionID) => void inbox.open(sessionID)), addTreePanel(ctx), addTreeAttention(ctx)];
|
|
1350
1743
|
return () => {
|
|
1351
1744
|
for (const cleanup of cleanups.toReversed())
|