bom-pi 0.0.0-stage → 0.0.2

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.
Files changed (131) hide show
  1. package/README.md +33 -2
  2. package/apps/README.md +7 -0
  3. package/apps/web/README.md +21 -0
  4. package/apps/web/pocket-adapter.mjs +323 -0
  5. package/apps/web/public/LICENSE.pocket +21 -0
  6. package/apps/web/public/activity-fold.js +28 -0
  7. package/apps/web/public/agent-now.js +37 -0
  8. package/apps/web/public/agent-tile.js +79 -0
  9. package/apps/web/public/app.js +975 -0
  10. package/apps/web/public/apple-touch-icon.png +0 -0
  11. package/apps/web/public/artifact-bridge.js +687 -0
  12. package/apps/web/public/avatar.js +87 -0
  13. package/apps/web/public/back.js +554 -0
  14. package/apps/web/public/box-keys.js +216 -0
  15. package/apps/web/public/browser-panel.js +1097 -0
  16. package/apps/web/public/browser.js +68 -0
  17. package/apps/web/public/calls.js +139 -0
  18. package/apps/web/public/chat.js +919 -0
  19. package/apps/web/public/clock.js +34 -0
  20. package/apps/web/public/commands.js +578 -0
  21. package/apps/web/public/composer.js +2068 -0
  22. package/apps/web/public/diff-parse.js +672 -0
  23. package/apps/web/public/diff.js +1646 -0
  24. package/apps/web/public/dock.js +299 -0
  25. package/apps/web/public/extension-ui.js +2734 -0
  26. package/apps/web/public/files-panel.js +775 -0
  27. package/apps/web/public/files.js +368 -0
  28. package/apps/web/public/fonts/FiraCode-VF.woff2 +0 -0
  29. package/apps/web/public/fonts/Geist.woff2 +0 -0
  30. package/apps/web/public/fonts/IBMPlexMono-Variable-Italic.woff2 +0 -0
  31. package/apps/web/public/fonts/IBMPlexMono-Variable.woff2 +0 -0
  32. package/apps/web/public/fonts/InterVariable-Italic.woff2 +0 -0
  33. package/apps/web/public/fonts/InterVariable.woff2 +0 -0
  34. package/apps/web/public/fonts/Iosevka-Bold.woff2 +0 -0
  35. package/apps/web/public/fonts/Iosevka-BoldItalic.woff2 +0 -0
  36. package/apps/web/public/fonts/Iosevka-Italic.woff2 +0 -0
  37. package/apps/web/public/fonts/Iosevka-Regular.woff2 +0 -0
  38. package/apps/web/public/fonts/Iosevka-SemiBold.woff2 +0 -0
  39. package/apps/web/public/fonts/Iosevka-SemiBoldItalic.woff2 +0 -0
  40. package/apps/web/public/fonts/JetBrainsMono-Italic.woff2 +0 -0
  41. package/apps/web/public/fonts/JetBrainsMono.woff2 +0 -0
  42. package/apps/web/public/fonts/NerdFontsSymbolsMono.woff2 +0 -0
  43. package/apps/web/public/fonts/SOURCES.md +69 -0
  44. package/apps/web/public/gestures.js +610 -0
  45. package/apps/web/public/highlight.js +480 -0
  46. package/apps/web/public/history.js +70 -0
  47. package/apps/web/public/home.js +169 -0
  48. package/apps/web/public/icon-192.png +0 -0
  49. package/apps/web/public/icon-512.png +0 -0
  50. package/apps/web/public/icon-maskable-512.png +0 -0
  51. package/apps/web/public/icon.svg +16 -0
  52. package/apps/web/public/index.html +71 -0
  53. package/apps/web/public/launcher.js +1293 -0
  54. package/apps/web/public/linked-sessions.js +55 -0
  55. package/apps/web/public/live.js +39 -0
  56. package/apps/web/public/manifest.webmanifest +32 -0
  57. package/apps/web/public/model-refresh.js +73 -0
  58. package/apps/web/public/notify.js +251 -0
  59. package/apps/web/public/peeks.js +920 -0
  60. package/apps/web/public/pip-attachments.js +182 -0
  61. package/apps/web/public/pip-window.js +141 -0
  62. package/apps/web/public/pip.js +1217 -0
  63. package/apps/web/public/quote-reply.js +109 -0
  64. package/apps/web/public/quote-text.js +31 -0
  65. package/apps/web/public/rich.js +403 -0
  66. package/apps/web/public/run-frame.js +106 -0
  67. package/apps/web/public/safe-mode.js +71 -0
  68. package/apps/web/public/seen.js +169 -0
  69. package/apps/web/public/session-tree.js +117 -0
  70. package/apps/web/public/sessions.js +2695 -0
  71. package/apps/web/public/share.js +126 -0
  72. package/apps/web/public/sheets/appearance.js +820 -0
  73. package/apps/web/public/sheets/branch.js +353 -0
  74. package/apps/web/public/sheets/extensions.js +818 -0
  75. package/apps/web/public/sheets/file.js +318 -0
  76. package/apps/web/public/sheets/find.js +250 -0
  77. package/apps/web/public/sheets/folder.js +680 -0
  78. package/apps/web/public/sheets/gallery.js +2340 -0
  79. package/apps/web/public/sheets/hooks.js +350 -0
  80. package/apps/web/public/sheets/memory.js +531 -0
  81. package/apps/web/public/sheets/menu.js +580 -0
  82. package/apps/web/public/sheets/message.js +220 -0
  83. package/apps/web/public/sheets/model.js +412 -0
  84. package/apps/web/public/sheets/people.js +414 -0
  85. package/apps/web/public/sheets/pi-sessions.js +264 -0
  86. package/apps/web/public/sheets/providers.js +525 -0
  87. package/apps/web/public/sheets/running.js +130 -0
  88. package/apps/web/public/sheets/schedules.js +71 -0
  89. package/apps/web/public/sheets/session.js +249 -0
  90. package/apps/web/public/sheets/shortcuts.js +101 -0
  91. package/apps/web/public/sheets/spend.js +150 -0
  92. package/apps/web/public/sheets/text.js +53 -0
  93. package/apps/web/public/sheets/viewers.js +363 -0
  94. package/apps/web/public/sheets/worktree.js +50 -0
  95. package/apps/web/public/sheets.js +410 -0
  96. package/apps/web/public/signin.js +57 -0
  97. package/apps/web/public/slots.js +208 -0
  98. package/apps/web/public/store.js +1760 -0
  99. package/apps/web/public/style.css +18625 -0
  100. package/apps/web/public/subagents-board.js +879 -0
  101. package/apps/web/public/subagents.js +273 -0
  102. package/apps/web/public/sw.js +169 -0
  103. package/apps/web/public/taps.js +26 -0
  104. package/apps/web/public/theme-surface.d.ts +4 -0
  105. package/apps/web/public/theme-surface.js +266 -0
  106. package/apps/web/public/theme.js +845 -0
  107. package/apps/web/public/themes/lantern/colors.toml +25 -0
  108. package/apps/web/public/themes/lantern/direction.css +60 -0
  109. package/apps/web/public/themes/lantern/frame.css +254 -0
  110. package/apps/web/public/themes/lantern/theme.css +10 -0
  111. package/apps/web/public/themes/lantern/theme.json +7 -0
  112. package/apps/web/public/themes/pocket/theme.css +189 -0
  113. package/apps/web/public/themes/pocket/theme.json +6 -0
  114. package/apps/web/public/themes.js +556 -0
  115. package/apps/web/public/transcript.js +3348 -0
  116. package/apps/web/public/trust.js +295 -0
  117. package/apps/web/public/turns.js +507 -0
  118. package/apps/web/public/ui.js +1910 -0
  119. package/apps/web/public/voice.js +345 -0
  120. package/apps/web/server.mjs +774 -0
  121. package/package.json +43 -4
  122. package/packages/cli/src/bom-pi.mjs +452 -0
  123. package/packages/cli/templates/blank.factory.json +7 -0
  124. package/packages/cli/templates/dev.factory.json +11 -0
  125. package/packages/cli/templates/docs.factory.json +12 -0
  126. package/packages/cli/templates/ops.factory.json +14 -0
  127. package/packages/cli/templates/triage.factory.json +10 -0
  128. package/packages/core/src/daemon.mjs +2047 -0
  129. package/packages/core/src/limit-gate.mjs +192 -0
  130. package/packages/core/src/rpc-client.mjs +66 -0
  131. package/systemd/README.md +11 -0
@@ -0,0 +1,531 @@
1
+ // Memory: the lessons Pi keeps across sessions, for this session's project and for everywhere.
2
+ import { useEffect, useState } from "preact/hooks";
3
+ import { api, attempt, closeSheet, navigate, notify, store } from "../store.js";
4
+ import { html, Sheet, timeAgo } from "../ui.js";
5
+
6
+ /** Memory needs its extension, and a conversation to say which project: `id`, or the one open. */
7
+ export const memoryAvailable = (id) =>
8
+ (id ?? store.state.conversationId) !== null &&
9
+ store.state.server?.extensions?.includes("pocket-memory") === true;
10
+
11
+ const path = (id, rest = "", pendingOnly = false) =>
12
+ `c/${id}/memory${rest}${pendingOnly ? "?pending=all" : ""}`;
13
+
14
+ /** The lessons last fetched, keyed by person, conversation, and view: the menu can show its hint while asking again. */
15
+ let last = null;
16
+ /**
17
+ * The fetch under way for each conversation, `{ rev, promise }`: the sheet, the menu, and each memory call ask once
18
+ * together, PiP's calls and the open session's too.
19
+ */
20
+ const asking = new Map();
21
+
22
+ function fetchLessons(id, rev, key, pendingOnly) {
23
+ const fetching = asking.get(key);
24
+
25
+ if (fetching?.rev === rev) {
26
+ return fetching.promise;
27
+ }
28
+
29
+ const asked = { rev, promise: api(path(id, "", pendingOnly)) };
30
+
31
+ asking.set(key, asked);
32
+ // A fetch that failed is not kept: the next one asks again.
33
+ asked.promise.catch(() => {
34
+ if (asking.get(key) === asked) {
35
+ asking.delete(key);
36
+ }
37
+ });
38
+
39
+ return asked.promise;
40
+ }
41
+
42
+ /**
43
+ * The lessons that apply in conversation `id` (the one open when absent), fetched when it shows and again whenever
44
+ * they change on the server.
45
+ */
46
+ export function useMemory(conversationId, pendingOnly = false) {
47
+ const id = conversationId ?? store.state.conversationId;
48
+ const { me } = store.state;
49
+ const key = `${me?.id}:${me?.role}:${id}:${pendingOnly ? "pending" : "applying"}`;
50
+ // Followed here, not read from the render: a call's card in the conversation is not drawn again on every change.
51
+ const revision = (state) => `${state.memoryRev ?? 0}:${state.streamId ?? ""}`;
52
+ const [rev, setRev] = useState(() => revision(store.state));
53
+
54
+ useEffect(() => store.subscribe((state) => setRev(revision(state))), []);
55
+ const on = memoryAvailable(id) && (!pendingOnly || me?.role === "owner");
56
+ const [data, setData] = useState(last?.key === key ? last : null);
57
+ // Asked again a few times when a fetch fails (a dropped connection, a restart), rather than loading for ever.
58
+ const [retry, setRetry] = useState(0);
59
+
60
+ useEffect(() => {
61
+ let live = true;
62
+ let timer;
63
+
64
+ if (!on) {
65
+ return undefined;
66
+ }
67
+
68
+ fetchLessons(id, rev, key, pendingOnly).then(
69
+ (fetched) => {
70
+ if (live) {
71
+ // An older running server ignores the new query: never label its scoped view as an all-project queue.
72
+ last = {
73
+ key,
74
+ data:
75
+ pendingOnly && fetched.pendingAll !== true
76
+ ? { restartRequired: true }
77
+ : fetched,
78
+ };
79
+ setData(last);
80
+ }
81
+ },
82
+ () => {
83
+ if (live && retry < 3) {
84
+ timer = setTimeout(() => setRetry(retry + 1), 2000 * (retry + 1));
85
+ }
86
+ },
87
+ );
88
+
89
+ return () => {
90
+ live = false;
91
+ clearTimeout(timer);
92
+ };
93
+ }, [key, rev, on, retry]);
94
+
95
+ return !on ? null : data?.key === key ? data.data : last?.key === key ? last.data : null;
96
+ }
97
+
98
+ /** Fetch again after a write here, without waiting for the server's word. */
99
+ function refresh() {
100
+ store.set((state) => ({ memoryRev: (state.memoryRev ?? 0) + 1 }));
101
+ }
102
+
103
+ /** The menu's hint: how many lessons apply here and everywhere, and for the owner how many wait for approval. */
104
+ export function memoryHint(data) {
105
+ if (!data) {
106
+ return "";
107
+ }
108
+
109
+ const live = data.lessons.filter((lesson) => !lesson.pending);
110
+ const here = live.filter((lesson) => lesson.scope === "project").length;
111
+ const everywhere = live.length - here;
112
+ const waiting = data.owner
113
+ ? data.lessons.filter((lesson) => lesson.pending || lesson.proposal).length
114
+ : 0;
115
+ const parts = [
116
+ data.project ? `${here} here` : null,
117
+ `${everywhere} everywhere`,
118
+ waiting > 0 ? `${waiting} to approve` : null,
119
+ ];
120
+
121
+ return parts.filter(Boolean).join(" · ");
122
+ }
123
+
124
+ /** Save a lesson by hand: for this project, or everywhere. */
125
+ export async function remember(text, scope = "project", conversationId) {
126
+ const id = conversationId ?? store.state.conversationId;
127
+ const lesson = await api(path(id), { text, scope });
128
+
129
+ refresh();
130
+
131
+ return lesson;
132
+ }
133
+
134
+ /** What the owner saw waiting, sent with Approve: the server approves only if it still waits. */
135
+ function approval(lesson) {
136
+ if (lesson.pending) {
137
+ return { text: lesson.text };
138
+ }
139
+
140
+ return lesson.proposal?.forget ? { forget: true } : { text: lesson.proposal?.text };
141
+ }
142
+
143
+ /** Who saved a lesson, and where it came from, in a line. */
144
+ function Source({ lesson }) {
145
+ const { me } = store.state;
146
+ const who = lesson.byPi ? "Pi" : lesson.by === me?.id ? "you" : lesson.byName;
147
+
148
+ const open = (event) => {
149
+ event.preventDefault();
150
+ closeSheet();
151
+ navigate(lesson.from.id);
152
+ };
153
+
154
+ return html`<div class="muted small lesson-source">
155
+ <span>${who}</span>
156
+ ${lesson.byPi && lesson.by !== me?.id && html`<span> for ${lesson.byName}</span>`}
157
+ ${
158
+ lesson.from &&
159
+ html` · <a href=${`/s/${lesson.from.id}`} onClick=${open}>${lesson.from.title}</a>`
160
+ }
161
+ <span> · ${timeAgo(lesson.updatedAt ?? lesson.createdAt)}</span>
162
+ ${(lesson.pending || lesson.proposal) && html`<span> · for ${reach(lesson)}</span>`}
163
+ </div>`;
164
+ }
165
+
166
+ /** Where a lesson applies, said where it waits: everywhere's reach every session and PiP. */
167
+ const reach = (lesson) =>
168
+ lesson.scope === "everywhere"
169
+ ? "everywhere: every session, and PiP"
170
+ : (lesson.projectName ?? "this project");
171
+
172
+ /** One lesson: its text, where it came from, and what this person may do with it. */
173
+ function Lesson({ lesson, data, conversationId, pendingOnly = false }) {
174
+ const id = conversationId ?? store.state.conversationId;
175
+ const [draft, setDraft] = useState(null);
176
+ const [forgetting, setForgetting] = useState(false);
177
+ const act = (rest, body = {}, done = "") =>
178
+ attempt(async () => {
179
+ // Fetched again either way: after a refusal (it changed since), the sheet shows what waits now.
180
+ try {
181
+ await api(path(id, `/${encodeURIComponent(lesson.id)}${rest}`, pendingOnly), body);
182
+ } finally {
183
+ refresh();
184
+ }
185
+
186
+ setDraft(null);
187
+ setForgetting(false);
188
+
189
+ if (done) {
190
+ notify("info", done);
191
+ }
192
+ });
193
+ const approving = data.owner && (lesson.pending || lesson.proposal);
194
+ const withdrawing = !data.owner && lesson.proposal?.mine;
195
+
196
+ if (draft !== null) {
197
+ return html`<div class="lesson">
198
+ <textarea
199
+ rows="3"
200
+ autofocus
201
+ maxlength=${data.limits.text}
202
+ value=${draft}
203
+ onInput=${(event) => setDraft(event.currentTarget.value)}
204
+ ></textarea>
205
+ <div class="row lesson-actions">
206
+ <button class="button small ghost" onClick=${() => setDraft(null)}>Cancel</button>
207
+ <button
208
+ class="button small primary"
209
+ disabled=${draft.trim() === ""}
210
+ onClick=${() => act("", { text: draft })}
211
+ >
212
+ Save
213
+ </button>
214
+ </div>
215
+ </div>`;
216
+ }
217
+
218
+ return html`<div class=${`lesson ${lesson.pending ? "pending" : ""}`}>
219
+ <div class="lesson-text">${lesson.text}</div>
220
+ ${
221
+ lesson.proposal &&
222
+ html`<div class="lesson-proposal">
223
+ <span class="muted small">
224
+ ${lesson.proposal.byPi ? "Pi" : lesson.proposal.mine ? "You" : lesson.proposal.byName} ${lesson.proposal.forget ? "proposes forgetting it." : "proposes:"}
225
+ </span>
226
+ ${lesson.proposal.text && html`<div class="lesson-text">${lesson.proposal.text}</div>`}
227
+ </div>`
228
+ }
229
+ ${
230
+ (lesson.pending || lesson.proposal) &&
231
+ !data.owner &&
232
+ html`<div class="muted small">Waits for the owner's approval; until then Pi goes on as before.</div>`
233
+ }
234
+ <div class="lesson-foot">
235
+ <${Source} lesson=${lesson} />
236
+ ${
237
+ (lesson.mine || approving || withdrawing) &&
238
+ html`<div class="row lesson-actions">
239
+ ${
240
+ approving &&
241
+ html`<button
242
+ class="button small primary"
243
+ onClick=${() =>
244
+ act(
245
+ "/approve",
246
+ approval(lesson),
247
+ "Approved: Pi goes by it from its next message.",
248
+ )}
249
+ >
250
+ Approve
251
+ </button>`
252
+ }
253
+ ${
254
+ (approving || withdrawing) &&
255
+ html`<button class="button small ghost" onClick=${() => act("/discard")}>
256
+ ${approving ? "Discard" : "Withdraw"}
257
+ </button>`
258
+ }
259
+ ${
260
+ lesson.mine &&
261
+ html`<button
262
+ class="button small ghost"
263
+ onClick=${() => setDraft(lesson.text)}
264
+ >
265
+ Edit
266
+ </button>`
267
+ }
268
+ ${
269
+ lesson.mine &&
270
+ !(approving && lesson.pending) &&
271
+ html`<button
272
+ class=${`button small ghost ${forgetting ? "danger" : ""}`}
273
+ onClick=${() => (forgetting ? act("/remove") : setForgetting(true))}
274
+ onBlur=${() => setForgetting(false)}
275
+ >
276
+ ${forgetting ? "Forget it" : "Forget"}
277
+ </button>`
278
+ }
279
+ </div>`
280
+ }
281
+ </div>
282
+ </div>`;
283
+ }
284
+
285
+ /** A group of lessons with its title, or nothing when it has none. */
286
+ function Group({ title, lessons, data, empty, conversationId, pendingOnly = false }) {
287
+ return html`<div class="group">
288
+ <div class="group-title">${title}</div>
289
+ ${lessons.length === 0 && empty && html`<p class="muted small">${empty}</p>`}
290
+ ${
291
+ lessons.length > 0 &&
292
+ html`<div class="sheet-list">
293
+ ${lessons.map(
294
+ (lesson) => html`<${Lesson}
295
+ key=${lesson.id}
296
+ lesson=${lesson}
297
+ data=${data}
298
+ conversationId=${conversationId}
299
+ pendingOnly=${pendingOnly}
300
+ />`,
301
+ )}
302
+ </div>`
303
+ }
304
+ </div>`;
305
+ }
306
+
307
+ /** The lessons Pi keeps: what waits for approval, this project's, and everywhere's; and one added by hand. */
308
+ export function MemorySheet({ draft: initial = "", conversationId, pendingOnly = false }) {
309
+ const id = conversationId ?? store.state.conversationId;
310
+ const data = useMemory(id, pendingOnly);
311
+ const [text, setText] = useState(initial);
312
+ const [scope, setScope] = useState("project");
313
+ const [busy, setBusy] = useState(false);
314
+
315
+ const title = pendingOnly ? "Pending memories" : "Memory";
316
+
317
+ if (pendingOnly && store.state.me?.role !== "owner") {
318
+ return html`<${Sheet} title=${title} onClose=${closeSheet}>
319
+ <p class="muted">Only the owner can review pending memories.</p>
320
+ <//>`;
321
+ }
322
+
323
+ if (!data) {
324
+ return html`<${Sheet} title=${title} onClose=${closeSheet}>
325
+ <p class="muted">Loading…</p>
326
+ <//>`;
327
+ }
328
+
329
+ if (pendingOnly && data.restartRequired) {
330
+ return html`<${Sheet} title=${title} onClose=${closeSheet}>
331
+ <p class="muted">
332
+ Restart the server from Menu → Restart server to review pending memories across projects.
333
+ </p>
334
+ <//>`;
335
+ }
336
+
337
+ const project = data.project;
338
+ const chosen = project ? scope : "everywhere";
339
+ const waiting = data.owner
340
+ ? data.lessons.filter((lesson) => lesson.pending || lesson.proposal)
341
+ : [];
342
+ const shown = data.lessons.filter((lesson) => !waiting.includes(lesson));
343
+
344
+ const add = () => {
345
+ if (busy || text.trim() === "") {
346
+ return;
347
+ }
348
+
349
+ setBusy(true);
350
+ attempt(async () => {
351
+ const lesson = await remember(text, chosen, id);
352
+
353
+ setText("");
354
+ notify(
355
+ "info",
356
+ lesson.pending
357
+ ? "Saved. Pi uses it once the owner approves it."
358
+ : "Saved. Pi gets it from its next message.",
359
+ );
360
+ }).finally(() => setBusy(false));
361
+ };
362
+
363
+ if (pendingOnly) {
364
+ return html`<${Sheet} title=${title} onClose=${closeSheet}>
365
+ <p class="muted small">
366
+ Memories waiting for your approval, across every project and everywhere.
367
+ Approve only what you want Pi to keep; until then Pi goes on as before.
368
+ </p>
369
+ <${Group}
370
+ title=${`To approve · ${waiting.length}`}
371
+ lessons=${waiting}
372
+ data=${data}
373
+ conversationId=${id}
374
+ pendingOnly=${true}
375
+ empty="No memories waiting for approval."
376
+ />
377
+ <//>`;
378
+ }
379
+
380
+ return html`<${Sheet} title=${title} onClose=${closeSheet}>
381
+ <p class="muted small">
382
+ Short lessons Pi keeps across sessions: how to build and test, gotchas, conventions, preferences. ${project ? html`Those for <b>${project.name}</b> go to every session in it, worktrees too; the rest go everywhere.` : "PiP keeps lessons for everywhere."} Pi proposes one only when asked, or when sure it will help in many later sessions, and the owner approves; yours count at once.
383
+ </p>
384
+ ${
385
+ data.canWrite &&
386
+ html`<div class="lesson-add">
387
+ <textarea
388
+ rows="2"
389
+ placeholder="e.g. Run the tests with npm test; the browser tests need Chromium."
390
+ maxlength=${data.limits.text}
391
+ value=${text}
392
+ onInput=${(event) => setText(event.currentTarget.value)}
393
+ onKeyDown=${(event) => {
394
+ if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
395
+ event.preventDefault();
396
+ add();
397
+ }
398
+ }}
399
+ ></textarea>
400
+ <div class="row">
401
+ ${
402
+ project &&
403
+ html`<div class="segmented grow" role="radiogroup" aria-label="For">
404
+ <button
405
+ role="radio"
406
+ aria-checked=${chosen === "project" ? "true" : "false"}
407
+ class=${chosen === "project" ? "on" : ""}
408
+ onClick=${() => setScope("project")}
409
+ >
410
+ This project
411
+ </button>
412
+ <button
413
+ role="radio"
414
+ aria-checked=${chosen === "everywhere" ? "true" : "false"}
415
+ class=${chosen === "everywhere" ? "on" : ""}
416
+ onClick=${() => setScope("everywhere")}
417
+ >
418
+ Everywhere
419
+ </button>
420
+ </div>`
421
+ }
422
+ <button
423
+ class="button primary"
424
+ disabled=${busy || text.trim() === ""}
425
+ onClick=${add}
426
+ >
427
+ Remember
428
+ </button>
429
+ </div>
430
+ </div>`
431
+ }
432
+ ${
433
+ waiting.length > 0 &&
434
+ html`<${Group}
435
+ title=${`To approve · ${waiting.length}`}
436
+ lessons=${waiting}
437
+ data=${data}
438
+ conversationId=${id}
439
+ />`
440
+ }
441
+ ${
442
+ project &&
443
+ html`<${Group}
444
+ title=${`This project: ${project.name}`}
445
+ lessons=${shown.filter((lesson) => lesson.scope === "project")}
446
+ data=${data}
447
+ conversationId=${id}
448
+ empty="Nothing yet."
449
+ />`
450
+ }
451
+ <${Group}
452
+ title="Everywhere"
453
+ lessons=${shown.filter((lesson) => lesson.scope === "everywhere")}
454
+ data=${data}
455
+ conversationId=${id}
456
+ empty="Nothing yet."
457
+ />
458
+ <//>`;
459
+ }
460
+
461
+ /**
462
+ * Under Pi's memory call: what became of the lesson it saved, changed, or forgot, in conversation `conversationId` (the
463
+ * one open when absent). While it waits, the owner reads it and approves or discards it right here, without opening
464
+ * the sheet.
465
+ */
466
+ export function LessonCall({ lessonId, conversationId }) {
467
+ const id = conversationId ?? store.state.conversationId;
468
+ const data = useMemory(id);
469
+
470
+ if (!data) {
471
+ return null;
472
+ }
473
+
474
+ const lesson = data.lessons.find((each) => each.id === lessonId);
475
+ // Fetched again either way: after a refusal (it changed since), the card shows what waits now.
476
+ const act = (rest, body, done) =>
477
+ attempt(async () => {
478
+ try {
479
+ await api(path(id, `/${encodeURIComponent(lessonId)}${rest}`), body);
480
+ } finally {
481
+ refresh();
482
+ }
483
+
484
+ notify("info", done);
485
+ });
486
+
487
+ // One waiting for the owner is hidden from others: say nothing rather than that it is gone.
488
+ if (!lesson) {
489
+ return data.owner
490
+ ? html`<div class="lesson-call muted small">No longer in memory.</div>`
491
+ : null;
492
+ }
493
+
494
+ if (!lesson.pending && !lesson.proposal) {
495
+ return html`<div class="lesson-call muted small">In memory: Pi goes by it.</div>`;
496
+ }
497
+
498
+ const what = lesson.pending
499
+ ? "A new lesson"
500
+ : lesson.proposal.forget
501
+ ? "Forgetting this lesson"
502
+ : "New text for this lesson";
503
+ const text = lesson.pending || lesson.proposal.forget ? lesson.text : lesson.proposal.text;
504
+
505
+ return html`<div class="lesson-call waiting">
506
+ <div class="grow">
507
+ <div>${what}, for ${reach(lesson)}, waits for ${data.owner ? "you" : "the owner"}:</div>
508
+ <div class="lesson-text">${text}</div>
509
+ </div>
510
+ ${
511
+ data.owner &&
512
+ html`<button
513
+ class="button small primary"
514
+ onClick=${() =>
515
+ act(
516
+ "/approve",
517
+ approval(lesson),
518
+ "Approved: Pi goes by it from its next message.",
519
+ )}
520
+ >
521
+ Approve
522
+ </button>
523
+ <button
524
+ class="button small ghost"
525
+ onClick=${() => act("/discard", {}, "Discarded.")}
526
+ >
527
+ Discard
528
+ </button>`
529
+ }
530
+ </div>`;
531
+ }