@nanocollective/roster 0.1.0-alpha.27 → 0.1.0-alpha.29
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/dist/cli.js +6 -2
- package/package.json +1 -1
- package/templates/portal/css/base.css +4 -0
- package/templates/portal/css/setup.css +8 -0
- package/templates/portal/index.html +1 -0
- package/templates/portal/js/app.js +38 -1
- package/templates/portal/js/inflight.js +18 -0
- package/templates/portal/js/views/credential.js +31 -45
- package/templates/portal/js/views/hire.js +1 -1
- package/templates/portal/js/views/inbox.js +22 -34
- package/templates/portal/js/views/staff.js +45 -10
package/dist/cli.js
CHANGED
|
@@ -6608,15 +6608,19 @@ async function portalCommand(argv) {
|
|
|
6608
6608
|
return;
|
|
6609
6609
|
}
|
|
6610
6610
|
const agentId = String(org.agent?.id ?? org.agent ?? "claude-code-action");
|
|
6611
|
-
const [plan2, orgSecret] = await Promise.all([
|
|
6611
|
+
const [plan2, orgSecret, onRepos] = await Promise.all([
|
|
6612
6612
|
planCredential(org.org, name, brains),
|
|
6613
|
-
readOrgSecret(org.org, name)
|
|
6613
|
+
readOrgSecret(org.org, name),
|
|
6614
|
+
Promise.all(brains.map((b) => api(`repos/${b}/actions/secrets/${name}`).then((r) => r.ok)))
|
|
6614
6615
|
]);
|
|
6616
|
+
const repoSecrets = brains.filter((_, i) => onRepos[i]);
|
|
6615
6617
|
json(res, {
|
|
6616
6618
|
name,
|
|
6617
6619
|
brains,
|
|
6618
6620
|
plan: plan2,
|
|
6619
6621
|
orgSecret,
|
|
6622
|
+
repoSecrets,
|
|
6623
|
+
stored: Boolean(orgSecret) || brains.length > 0 && repoSecrets.length === brains.length,
|
|
6620
6624
|
howTo: AGENTS.find((a) => a.id === agentId)?.howTo
|
|
6621
6625
|
});
|
|
6622
6626
|
return;
|
package/package.json
CHANGED
|
@@ -285,3 +285,7 @@ dialog.sheet[open]{animation:dlgin .18s var(--ease)}
|
|
|
285
285
|
dialog.sheet::backdrop{background:rgba(0,0,0,.35);backdrop-filter:blur(8px)}
|
|
286
286
|
.sheetbody{padding:22px 24px 26px;overflow:auto;max-height:calc(100vh - 50px)}
|
|
287
287
|
.sheettitle{margin:0 0 14px;font:600 18px var(--sans);letter-spacing:-.02em}
|
|
288
|
+
.sheetloading{display:grid;place-items:center;min-height:260px}
|
|
289
|
+
.spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--line);border-top-color:var(--accent);
|
|
290
|
+
animation:spin .7s linear infinite}
|
|
291
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
@@ -183,3 +183,11 @@
|
|
|
183
183
|
.pastenotes .md{border-left:3px solid var(--line);padding-left:12px}
|
|
184
184
|
.pastefile .diff{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
185
185
|
.warnline{margin:10px 0 0;padding:8px 11px;border-radius:var(--r);background:color-mix(in srgb,var(--warn) 12%,transparent);color:var(--ink);font-size:13px}
|
|
186
|
+
|
|
187
|
+
/* A single-line text box, matching the question fields. */
|
|
188
|
+
.textfield{width:100%;background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
|
|
189
|
+
border-radius:var(--r);padding:8px 10px;font:13px var(--sans);outline:none}
|
|
190
|
+
.textfield:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
|
|
191
|
+
.credform{display:grid;gap:10px}
|
|
192
|
+
.credform .row{margin:0}
|
|
193
|
+
.okline{margin:0 0 8px;color:var(--accent);font:600 13px var(--sans)}
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
<span class="ic" data-icon="inbox"></span> Inbox
|
|
36
36
|
<span class="n" id="inboxcount"></span>
|
|
37
37
|
</button>
|
|
38
|
+
<div class="staffviews" id="inboxsub" hidden></div>
|
|
38
39
|
<!-- "Pending work", not "Pull requests": what is behind it is finished work waiting on you,
|
|
39
40
|
and the word for how it is delivered is not the word for what it is. -->
|
|
40
41
|
<button class="nav" id="prsnav" data-view="prs" title="Finished work waiting on you to merge">
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
syncNotice,
|
|
15
15
|
} from "./refresh.js";
|
|
16
16
|
import { onRender } from "./router.js";
|
|
17
|
-
import { ORG_WIDE, S, VIEWS, applyHash, readHash, writeHash } from "./state.js";
|
|
17
|
+
import { ORG_WIDE, S, VIEWS, applyHash, humanLabel, readHash, writeHash } from "./state.js";
|
|
18
18
|
import { viewBrain } from "./views/brain.js";
|
|
19
19
|
import { viewChanged } from "./views/changed.js";
|
|
20
20
|
import { viewDocs } from "./views/docs.js";
|
|
@@ -254,6 +254,7 @@ function initStaffFold() {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
function paintSidebar() {
|
|
257
|
+
paintInboxLinks();
|
|
257
258
|
const host = $("#stafflist");
|
|
258
259
|
host.replaceChildren();
|
|
259
260
|
paintedRoster = S.data.staff.map((s) => s.handle).join(" ");
|
|
@@ -315,7 +316,43 @@ function paintSidebar() {
|
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
/** Which rows are lit, and which staff member is unfolded. */
|
|
319
|
+
/* The inbox's filters, as links under Inbox: everything, what is on you, decisions, then one
|
|
320
|
+
per staff member. They replaced two dropdowns on the Inbox screen itself. */
|
|
321
|
+
function paintInboxLinks() {
|
|
322
|
+
const sub = $("#inboxsub");
|
|
323
|
+
if (!sub) return;
|
|
324
|
+
sub.replaceChildren();
|
|
325
|
+
const links = [
|
|
326
|
+
["", "", "All"],
|
|
327
|
+
["mine", "", "On " + humanLabel()],
|
|
328
|
+
["decision", "", "Decisions"],
|
|
329
|
+
...S.data.staff.map((s) => ["", s.handle, s.name]),
|
|
330
|
+
];
|
|
331
|
+
for (const [filter, who, label] of links) {
|
|
332
|
+
const b = el("button", { className: "nav subnav inboxlink", textContent: label });
|
|
333
|
+
b.dataset.filter = filter;
|
|
334
|
+
b.dataset.who = who;
|
|
335
|
+
b.onclick = () => {
|
|
336
|
+
S.view = "inbox";
|
|
337
|
+
S.inboxFilter = filter;
|
|
338
|
+
S.inboxStaff = who;
|
|
339
|
+
S.inboxOpen = null;
|
|
340
|
+
render();
|
|
341
|
+
};
|
|
342
|
+
sub.append(b);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
318
346
|
function markSidebar() {
|
|
347
|
+
const sub = $("#inboxsub");
|
|
348
|
+
if (sub) sub.hidden = S.view !== "inbox";
|
|
349
|
+
for (const b of document.querySelectorAll(".inboxlink")) {
|
|
350
|
+
const on =
|
|
351
|
+
S.view === "inbox" &&
|
|
352
|
+
b.dataset.filter === (S.inboxFilter || "") &&
|
|
353
|
+
b.dataset.who === (S.inboxStaff || "");
|
|
354
|
+
b.setAttribute("aria-current", String(on));
|
|
355
|
+
}
|
|
319
356
|
for (const b of document.querySelectorAll(".nav")) {
|
|
320
357
|
if (!b.dataset.view) continue;
|
|
321
358
|
const mine = !b.dataset.staff || b.dataset.staff === S.staffHandle;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* How many requests the page has open. The setup dialog waits for this to reach zero before it
|
|
2
|
+
* shows itself, because the list it draws is only final once every answer is back. */
|
|
3
|
+
|
|
4
|
+
let open = 0;
|
|
5
|
+
|
|
6
|
+
if (typeof globalThis.fetch === "function" && !globalThis.fetch.counted) {
|
|
7
|
+
const real = globalThis.fetch.bind(globalThis);
|
|
8
|
+
const counted = (...args) => {
|
|
9
|
+
open++;
|
|
10
|
+
return real(...args).finally(() => {
|
|
11
|
+
open--;
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
counted.counted = true;
|
|
15
|
+
globalThis.fetch = counted;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const pending = () => open;
|
|
@@ -12,11 +12,11 @@ export function credentialPanel(opts = {}) {
|
|
|
12
12
|
if (!opts.bare) box.append(el("b", { textContent: "Agent credential" }));
|
|
13
13
|
const body = el("div");
|
|
14
14
|
box.append(body);
|
|
15
|
-
body.append(el("p", { className: "sub", textContent: "
|
|
15
|
+
body.append(el("p", { className: "sub", textContent: "Checking…" }));
|
|
16
16
|
|
|
17
17
|
getCredential()
|
|
18
18
|
.then((c) => {
|
|
19
|
-
opts.onStatus?.(Boolean(c.orgSecret));
|
|
19
|
+
opts.onStatus?.(Boolean(c.stored ?? c.orgSecret));
|
|
20
20
|
draw(body, c, opts);
|
|
21
21
|
})
|
|
22
22
|
.catch((err) => body.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) })));
|
|
@@ -25,74 +25,60 @@ export function credentialPanel(opts = {}) {
|
|
|
25
25
|
|
|
26
26
|
function draw(body, c, opts = {}) {
|
|
27
27
|
body.replaceChildren();
|
|
28
|
-
if (c.howTo) body.append(el("p", { textContent: "Where to get one: " + c.howTo }));
|
|
29
28
|
|
|
30
29
|
if (!(c.brains ?? []).length) {
|
|
31
30
|
// Inside a step, the step's own hint already says when this opens.
|
|
32
|
-
if (opts.bare)
|
|
33
|
-
body.append(
|
|
34
|
-
el("p", {
|
|
35
|
-
className: "sub",
|
|
36
|
-
textContent:
|
|
37
|
-
"Available after your first hire. You only add it once.",
|
|
38
|
-
}),
|
|
39
|
-
);
|
|
31
|
+
if (!opts.bare) body.append(el("p", { className: "sub", textContent: "Available after your first hire. You only add it once." }));
|
|
40
32
|
return;
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
body.append(
|
|
54
|
-
el("p", {
|
|
55
|
-
className: "sub",
|
|
56
|
-
textContent:
|
|
57
|
-
(c.plan.mode === "org"
|
|
58
|
-
? "Stored as one org secret, " + c.name + ", shared with " + c.brains.join(", ") + ". "
|
|
59
|
-
: "Stored as " + c.name + " on each of " + c.brains.join(", ") + ", because " + c.plan.reason + ". ") +
|
|
60
|
-
"It's never written to disk.",
|
|
61
|
-
}),
|
|
35
|
+
const form = el("div", { className: "credform" });
|
|
36
|
+
const input = el("input", { type: "password", placeholder: "Paste it here", autocomplete: "off", className: "textfield" });
|
|
37
|
+
const go = el("button", { className: "btn primary", textContent: "Save" });
|
|
38
|
+
const out = el("div");
|
|
39
|
+
form.append(
|
|
40
|
+
...(c.howTo ? [el("p", { className: "sub", textContent: "To get one, " + c.howTo })] : []),
|
|
41
|
+
input,
|
|
42
|
+
el("div", { className: "row" }, [go]),
|
|
43
|
+
out,
|
|
62
44
|
);
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
// Stored already, on the org or on every brain repo: say so, and keep replacing it one click away.
|
|
47
|
+
if (c.stored ?? c.orgSecret) {
|
|
48
|
+
const replace = el("button", { className: "ghbtn", textContent: "Replace it" });
|
|
49
|
+
replace.onclick = () => {
|
|
50
|
+
replace.remove();
|
|
51
|
+
form.hidden = false;
|
|
52
|
+
input.focus?.();
|
|
53
|
+
};
|
|
54
|
+
form.hidden = true;
|
|
55
|
+
body.append(el("p", { className: "okline", textContent: "✓ Stored as " + c.name + "." }), replace, form);
|
|
56
|
+
} else {
|
|
57
|
+
body.append(form);
|
|
58
|
+
}
|
|
69
59
|
|
|
70
60
|
go.onclick = async () => {
|
|
71
|
-
const value = input.value.trim();
|
|
61
|
+
const value = String(input.value ?? "").trim();
|
|
72
62
|
if (!value) {
|
|
73
|
-
input.focus();
|
|
63
|
+
input.focus?.();
|
|
74
64
|
return;
|
|
75
65
|
}
|
|
76
66
|
go.disabled = true;
|
|
77
|
-
|
|
67
|
+
go.textContent = "Saving…";
|
|
78
68
|
try {
|
|
79
69
|
const r = await storeCredential(value, false);
|
|
80
70
|
input.value = "";
|
|
81
71
|
if (!r.ok) throw new Error(r.error);
|
|
82
72
|
opts.onStatus?.(true);
|
|
83
73
|
out.replaceChildren(
|
|
84
|
-
...(r.fellBack ? [el("p", { className: "
|
|
85
|
-
el("p", {
|
|
86
|
-
textContent:
|
|
87
|
-
r.mode === "org"
|
|
88
|
-
? "Saved."
|
|
89
|
-
: "Saved on " + r.repos.join(", ") + ".",
|
|
90
|
-
}),
|
|
74
|
+
...(r.fellBack ? [el("p", { className: "sub", textContent: "Saved on each repo instead: " + r.fellBack })] : []),
|
|
75
|
+
el("p", { className: "okline", textContent: "✓ Saved." }),
|
|
91
76
|
);
|
|
92
77
|
} catch (err) {
|
|
93
78
|
out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
94
79
|
} finally {
|
|
95
80
|
go.disabled = false;
|
|
81
|
+
go.textContent = "Save";
|
|
96
82
|
}
|
|
97
83
|
};
|
|
98
84
|
}
|
|
@@ -203,7 +203,7 @@ export function hireFlow(o) {
|
|
|
203
203
|
hireStep(hire.body, role, o.onHired);
|
|
204
204
|
for (const t of [app, charter, cred, run]) t.setLocked("Hire first");
|
|
205
205
|
getCredential()
|
|
206
|
-
.then((c) => credStatus(Boolean(c.orgSecret)))
|
|
206
|
+
.then((c) => credStatus(Boolean(c.stored ?? c.orgSecret)))
|
|
207
207
|
.catch(() => {});
|
|
208
208
|
return box;
|
|
209
209
|
}
|
|
@@ -122,37 +122,25 @@ function inboxScreen(m, opts) {
|
|
|
122
122
|
placeholder: "Filter by title, label, repo…",
|
|
123
123
|
value: S.query,
|
|
124
124
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
el("option", { value: "all", textContent: "Open and closed" }),
|
|
141
|
-
);
|
|
142
|
-
state.value = S.inboxState;
|
|
143
|
-
|
|
144
|
-
const whose = el("select", {
|
|
145
|
-
title: "Whose work: their brain repo, anything their bot wrote, and anything addressed to them",
|
|
146
|
-
});
|
|
147
|
-
whose.append(
|
|
148
|
-
el("option", { value: "", textContent: "Everyone" }),
|
|
149
|
-
...S.data.staff.map((s) => el("option", { value: s.handle, textContent: s.name })),
|
|
150
|
-
);
|
|
151
|
-
whose.value = S.inboxStaff;
|
|
125
|
+
/* Open or closed, as a toggle. Who and what (a staff member, what is on you, decisions)
|
|
126
|
+
moved to the sidebar under Inbox, where a row of dropdowns used to be. */
|
|
127
|
+
if (S.inboxState === "all") S.inboxState = "";
|
|
128
|
+
const toggle = el("div", { className: "modes inboxstate" });
|
|
129
|
+
for (const [value, label] of [["", "Open"], ["closed", "Closed"]]) {
|
|
130
|
+
const b = el("button", { className: "mode", textContent: label });
|
|
131
|
+
b.classList.toggle("on", (S.inboxState || "") === value);
|
|
132
|
+
b.onclick = () => {
|
|
133
|
+
S.inboxState = value;
|
|
134
|
+
for (const x of toggle.children) x.classList.toggle("on", x === b);
|
|
135
|
+
writeHash(false);
|
|
136
|
+
paint();
|
|
137
|
+
};
|
|
138
|
+
toggle.append(b);
|
|
139
|
+
}
|
|
152
140
|
|
|
153
141
|
const refresh = el("button", { className: "iconbtn", title: "Refresh from GitHub" });
|
|
154
142
|
refresh.innerHTML = '<span class="sync">' + iconHTML("refresh") + "</span>";
|
|
155
|
-
const controls = [search,
|
|
143
|
+
const controls = [search, toggle, refresh];
|
|
156
144
|
// A pull request comes from a branch, so there is nothing here that could open one.
|
|
157
145
|
if (!opts.prs) {
|
|
158
146
|
const newBtn = el("button", { className: "ghbtn", textContent: "New issue" });
|
|
@@ -171,9 +159,6 @@ function inboxScreen(m, opts) {
|
|
|
171
159
|
m.append(split);
|
|
172
160
|
|
|
173
161
|
search.oninput = () => { S.query = search.value; writeHash(false); paint(); };
|
|
174
|
-
scope.onchange = () => { S.inboxFilter = scope.value; writeHash(false); paint(); };
|
|
175
|
-
whose.onchange = () => { S.inboxStaff = whose.value; writeHash(false); paint(); };
|
|
176
|
-
state.onchange = () => { S.inboxState = state.value; writeHash(false); paint(); };
|
|
177
162
|
refresh.onclick = () => { refresh.classList.add("spin"); load(true); };
|
|
178
163
|
|
|
179
164
|
if (S.inbox) { stampCounts(); paint(); restore(); } else load(false);
|
|
@@ -245,7 +230,9 @@ function inboxScreen(m, opts) {
|
|
|
245
230
|
// "On you" is on any of the humans this org answers to, not only the first one.
|
|
246
231
|
const onAHuman = (i) => (i.assignees ?? []).some(isHuman);
|
|
247
232
|
|
|
248
|
-
|
|
233
|
+
// The sidebar's filters are the inbox's. Pending work has no control for them, so it ignores them.
|
|
234
|
+
const staffPick = opts.prs ? "" : S.inboxStaff;
|
|
235
|
+
const whoseStaff = staffPick ? S.data.staff.find((s) => s.handle === staffPick) : null;
|
|
249
236
|
/* The two screens partition the org rather than overlap on it. Pending work is every pull
|
|
250
237
|
request; the inbox is everything else. An inbox that also listed the PRs said the same
|
|
251
238
|
thing twice and made the badge beside it a number you could not act on. */
|
|
@@ -261,8 +248,9 @@ function inboxScreen(m, opts) {
|
|
|
261
248
|
(i) => !q || (i.title + " " + i.repo + " " + i.labels.join(" ") + " #" + i.number)
|
|
262
249
|
.toLowerCase().includes(q),
|
|
263
250
|
);
|
|
264
|
-
|
|
265
|
-
|
|
251
|
+
const filter = opts.prs ? "" : S.inboxFilter;
|
|
252
|
+
if (filter === "mine") items = items.filter(onAHuman);
|
|
253
|
+
else if (filter === "decision") items = items.filter((i) => i.labels.includes("decision"));
|
|
266
254
|
/* `s=pr` was the inbox's "Open work (PRs)" scope, and Pending work is now that screen. An
|
|
267
255
|
old bookmark carrying it is ignored rather than honoured into an empty list. */
|
|
268
256
|
|
|
@@ -7,12 +7,38 @@
|
|
|
7
7
|
|
|
8
8
|
import { post } from "../api.js";
|
|
9
9
|
import { askYes, sheet } from "../dialog.js";
|
|
10
|
+
import { pending } from "../inflight.js";
|
|
10
11
|
import { ago, el, esc } from "../dom.js";
|
|
11
12
|
import { whatsLeft, sentence } from "../readiness.js";
|
|
12
13
|
import { refreshAll } from "../refresh.js";
|
|
13
14
|
import { S } from "../state.js";
|
|
14
15
|
import { hireFlow, line, rolePicker } from "./hire.js";
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Resolves once nothing is loading and `node` has stopped changing for a moment, or after a few
|
|
19
|
+
* seconds regardless. Quiet alone was not enough: while GitHub was still answering, nothing
|
|
20
|
+
* changed, and the half-built list was shown.
|
|
21
|
+
*/
|
|
22
|
+
function settled(node, quiet = 250, most = 6000) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
if (typeof MutationObserver !== "function") return resolve();
|
|
25
|
+
let lastChange = Date.now();
|
|
26
|
+
const obs = new MutationObserver(() => {
|
|
27
|
+
lastChange = Date.now();
|
|
28
|
+
});
|
|
29
|
+
obs.observe(node, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
30
|
+
const started = Date.now();
|
|
31
|
+
const tick = setInterval(() => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if ((pending() === 0 && now - lastChange >= quiet) || now - started >= most) {
|
|
34
|
+
clearInterval(tick);
|
|
35
|
+
obs.disconnect();
|
|
36
|
+
resolve();
|
|
37
|
+
}
|
|
38
|
+
}, 50);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
16
42
|
/* The open dialog, if any. It lives on <body>, so it outlives a repaint of this screen, and a
|
|
17
43
|
background refresh leaves whatever was typed into it alone. */
|
|
18
44
|
let current = null;
|
|
@@ -31,18 +57,27 @@ export function viewStaff(m, o = {}) {
|
|
|
31
57
|
/* Tasks open in a dialog. Where there is no dialog element (the test shim) they draw into
|
|
32
58
|
the pane instead, which is how this screen always drew them. */
|
|
33
59
|
function present(node) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
60
|
+
const spin = el("div", { className: "sheetloading" }, [el("span", { className: "spinner" })]);
|
|
61
|
+
if (current?.isOpen()) current.set(spin);
|
|
62
|
+
else {
|
|
63
|
+
current = sheet({
|
|
64
|
+
node: spin,
|
|
65
|
+
onClose: () => {
|
|
66
|
+
current = null;
|
|
67
|
+
S.staffOpen = null;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (!current) {
|
|
71
|
+
pane.replaceChildren(node);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
37
74
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
},
|
|
75
|
+
/* The list is built from half a dozen answers arriving one by one, and showing each as it
|
|
76
|
+
landed meant the dialog reflowed four times. It is shown once it has stopped changing. */
|
|
77
|
+
const mine = current;
|
|
78
|
+
settled(node).then(() => {
|
|
79
|
+
if (mine.isOpen() && current === mine) mine.set(node);
|
|
44
80
|
});
|
|
45
|
-
if (!current) pane.replaceChildren(node);
|
|
46
81
|
}
|
|
47
82
|
function dismiss() {
|
|
48
83
|
if (current) current.close();
|