pi-roundtable-web 0.25.0 → 0.26.1
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/CHANGELOG.md +54 -0
- package/README.md +165 -12
- package/dist/account-zk4efb87.css +102 -0
- package/dist/chunk-account-ghq105t4.js +1 -0
- package/dist/chunk-action-menu-me0g9qsc.js +1 -0
- package/dist/{chunk-agent-0pm9p3nn.js → chunk-agent-rdv7ekrt.js} +1 -1
- package/dist/chunk-arranged-rooms-81rqhx9x.js +1 -0
- package/dist/chunk-character-creator-bd426yjw.js +1 -0
- package/dist/chunk-codeeditor-ajhgd33t.js +2 -0
- package/dist/chunk-confirm-p3mts8mx.js +1 -0
- package/dist/{chunk-connectors-anhghy3k.js → chunk-connectors-bkv3xn83.js} +1 -1
- package/dist/chunk-conversations-0e0vbxwr.js +1 -0
- package/dist/{chunk-drag-q4fx9b22.js → chunk-drag-peyb8t0k.js} +1 -1
- package/dist/{chunk-entry-he1embgp.js → chunk-entry-mv7q8br0.js} +1 -1
- package/dist/chunk-folder-list-era3tksh.js +1 -0
- package/dist/chunk-folds-hvws0bd5.js +1 -0
- package/dist/chunk-gate-hc7dd0fx.js +1 -0
- package/dist/chunk-group-scene-dkbpmbh5.js +1 -0
- package/dist/chunk-group-table-fqmxgdfw.js +1 -0
- package/dist/chunk-host-icons-4xde9ntk.js +1 -0
- package/dist/chunk-host-picture-014yzk20.js +1 -0
- package/dist/chunk-host-state-t56e5sdy.js +1 -0
- package/dist/chunk-host-ui-p1zy9nmp.js +1 -0
- package/dist/chunk-index-0fmmvgee.js +2 -0
- package/dist/chunk-index-0q15ysrb.js +1 -0
- package/dist/chunk-index-0xh24y25.js +2 -0
- package/dist/chunk-index-2yebd9tb.js +426 -0
- package/dist/chunk-index-4t0r3t3b.js +2 -0
- package/dist/chunk-index-5yzfrf7b.js +138 -0
- package/dist/{chunk-index-d5abvq7v.js → chunk-index-6twb7ccd.js} +2 -2
- package/dist/chunk-index-73gh7kgr.js +920 -0
- package/dist/chunk-index-7xffskfa.js +8 -0
- package/dist/chunk-index-aqw7cy5j.js +1 -0
- package/dist/chunk-index-b6ca02e8.js +2 -0
- package/dist/chunk-index-b8nvhcx5.js +2 -0
- package/dist/{chunk-index-bvfqjh77.js → chunk-index-cvbdf2ca.js} +2 -2
- package/dist/chunk-index-czdketze.js +2 -0
- package/dist/{chunk-index-9jqcskb4.js → chunk-index-dt9775ka.js} +5 -5
- package/dist/chunk-index-eg55mqqe.js +1 -0
- package/dist/chunk-index-gr6e0hse.js +2 -0
- package/dist/chunk-index-gyary7np.js +13 -0
- package/dist/chunk-index-j214tczc.js +1 -0
- package/dist/chunk-index-jd6632ef.js +2 -0
- package/dist/chunk-index-jw7dtshr.js +2 -0
- package/dist/{chunk-index-krtk6nn2.js → chunk-index-k7t14rwa.js} +2 -2
- package/dist/chunk-index-kwcx62wb.js +4 -0
- package/dist/chunk-index-m7fvw92f.js +2 -0
- package/dist/chunk-index-mmg788a1.js +2 -0
- package/dist/chunk-index-mq0dacwr.js +2 -0
- package/dist/chunk-index-p53ak25v.js +2 -0
- package/dist/{chunk-index-27qca4mr.js → chunk-index-pk1yxkv4.js} +2 -2
- package/dist/{chunk-index-9b1kt7qw.css → chunk-index-rbcz9jd4.css} +1 -1
- package/dist/{chunk-index-s681nny9.js → chunk-index-rmxbdmn2.js} +2 -2
- package/dist/chunk-index-rnxj3z8k.js +2 -0
- package/dist/chunk-index-snzzfhk3.js +2 -0
- package/dist/chunk-index-v3bmr0nw.js +2 -0
- package/dist/chunk-index-vcyzwtk2.js +2 -0
- package/dist/{chunk-index-zfd00hwb.js → chunk-index-w2j3njb6.js} +2 -2
- package/dist/chunk-index-w31c4yjv.js +2 -0
- package/dist/chunk-index-wrm8w165.js +2 -0
- package/dist/chunk-index-xh0mg9cw.js +2 -0
- package/dist/chunk-index-ybtvvx85.js +2 -0
- package/dist/chunk-index-yzsgsvnp.js +2 -0
- package/dist/{chunk-index-18gxy79n.js → chunk-index-z9x4cnzf.js} +2 -2
- package/dist/chunk-index-zrvg23jd.js +2 -0
- package/dist/chunk-index-zynq0b0w.js +50 -0
- package/dist/chunk-lang-css-88v3hp3d.js +1 -0
- package/dist/{chunk-lang-dockerfile-d5mzxs60.js → chunk-lang-dockerfile-k728ewsf.js} +1 -1
- package/dist/{chunk-lang-go-830fmhba.js → chunk-lang-go-wjwgay4v.js} +3 -3
- package/dist/chunk-lang-html-07d47es4.js +1 -0
- package/dist/chunk-lang-javascript-fmhhgzwd.js +1 -0
- package/dist/{chunk-lang-json-5eaarz7j.js → chunk-lang-json-7gd89mvs.js} +1 -1
- package/dist/chunk-lang-markdown-b6qbe8bt.js +6 -0
- package/dist/{chunk-lang-python-fjw84w10.js → chunk-lang-python-3v56y31b.js} +2 -2
- package/dist/{chunk-lang-rust-5rbp2zdk.js → chunk-lang-rust-kh0z74d1.js} +1 -1
- package/dist/{chunk-lang-shell-vzagpym3.js → chunk-lang-shell-9zv0zvbx.js} +1 -1
- package/dist/{chunk-lang-sql-0c2w9c2y.js → chunk-lang-sql-yw6bf3t7.js} +2 -2
- package/dist/{chunk-lang-toml-yr9fv6eb.js → chunk-lang-toml-tg1r96nq.js} +1 -1
- package/dist/chunk-lang-typescript-etjjvr2g.js +1 -0
- package/dist/{chunk-lang-xml-eze08p42.js → chunk-lang-xml-ev1mndd4.js} +1 -1
- package/dist/{chunk-lang-yaml-c4b6ft3y.js → chunk-lang-yaml-fz5qpb3r.js} +1 -1
- package/dist/chunk-layout-actions-48ecgtkg.js +1 -0
- package/dist/{chunk-menu-items-jrwxycr1.js → chunk-menu-items-1epy3h0t.js} +2 -2
- package/dist/chunk-mode-toggle-fxmfxsa4.js +1 -0
- package/dist/chunk-note-row-kd0fy7ee.js +75 -0
- package/dist/chunk-notes-yb8f057c.js +1 -0
- package/dist/chunk-notice-bell-wr4f1rph.js +1 -0
- package/dist/chunk-notices-x07da5bb.js +1 -0
- package/dist/chunk-onboarding-csrkbh9z.js +1 -0
- package/dist/chunk-overview-avc2d30t.js +1 -0
- package/dist/chunk-palette-9nhp95bz.js +1 -0
- package/dist/chunk-placement-items-q694dpgk.js +1 -0
- package/dist/chunk-profile-6qemf3m7.js +1 -0
- package/dist/chunk-prompt-history-7enzp2p3.js +1 -0
- package/dist/chunk-prompts-nv6mrn0q.js +1 -0
- package/dist/chunk-room-confirms-zz7n61y7.js +1 -0
- package/dist/chunk-room-menu-rr6nzap1.js +1 -0
- package/dist/chunk-rooms-menu-button-zbtwj47d.js +1 -0
- package/dist/chunk-schedules-rw57nr0m.js +1 -0
- package/dist/{chunk-seat-menu-jv4s7hmj.js → chunk-seat-menu-ys5v9abz.js} +1 -1
- package/dist/chunk-sheet-hosts-jhvaacst.js +1 -0
- package/dist/chunk-skill-editor-sz8an8qr.js +8 -0
- package/dist/chunk-skills-tnr1h16h.js +1 -0
- package/dist/chunk-switcher-list-17enhrse.js +1 -0
- package/dist/chunk-tab-dot-r6brdbmw.js +1 -0
- package/dist/chunk-table-r12cja6p.js +1 -0
- package/dist/chunk-table-seats-g3t392zz.js +1 -0
- package/dist/{chunk-table2d-4sje1fr0.js → chunk-table2d-p3ezsdcj.js} +1 -1
- package/dist/{chunk-table3d-h64skazp.js → chunk-table3d-4x2n2b00.js} +23 -23
- package/dist/chunk-thread-actions-qc6ezkha.js +1 -0
- package/dist/chunk-transcript-n2s6dher.js +1 -0
- package/dist/chunk-where-f3scdydc.js +1 -0
- package/dist/chunk-workspace-xs023bxm.js +1 -0
- package/dist/chunk-zh-tw-efvmk3fd.js +1 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +5 -4
- package/src/api-types.ts +356 -4
- package/src/app/app.ts +201 -32
- package/src/app/desk.ts +47 -22
- package/src/app/files.ts +73 -7
- package/src/app/presence.ts +2 -2
- package/src/app/protocol.ts +63 -1
- package/src/app/room-log.ts +153 -14
- package/src/app/rooms.ts +39 -0
- package/src/app/routes.ts +168 -28
- package/src/app/socket.ts +349 -81
- package/src/app/sockets.ts +108 -20
- package/src/app/surface.ts +72 -36
- package/src/app/threads.ts +31 -2
- package/src/app/uploads.ts +33 -7
- package/src/auth/authenticator.ts +77 -8
- package/src/auth/login-page.ts +4 -2
- package/src/auth/next.ts +52 -0
- package/src/auth/oidc-pages.ts +62 -0
- package/src/auth/oidc.ts +649 -0
- package/src/auth/sessions.ts +193 -0
- package/src/authz.ts +173 -0
- package/src/console-api.ts +122 -25
- package/src/console-server.ts +102 -12
- package/src/gate.ts +90 -0
- package/src/host/access.ts +161 -0
- package/src/host/cards.ts +112 -0
- package/src/host/context.ts +188 -0
- package/src/host/forms.ts +240 -0
- package/src/host/frames.ts +349 -0
- package/src/host/host-app.ts +178 -0
- package/src/host/host-context.ts +49 -0
- package/src/host/host-routes.ts +396 -0
- package/src/host/pages.ts +266 -0
- package/src/host/reads.ts +155 -0
- package/src/host/retention.ts +41 -0
- package/src/host/room-views.ts +137 -0
- package/src/host/rooms-service.ts +429 -0
- package/src/host/stored-json.ts +13 -0
- package/src/host/views.ts +106 -0
- package/src/i18n/zh-tw.ts +139 -0
- package/src/index.ts +15 -1
- package/src/notices/api.ts +43 -0
- package/src/notices/mount.ts +292 -0
- package/src/notices/service.ts +294 -0
- package/src/notices/store.ts +298 -0
- package/src/notices/urls.ts +38 -0
- package/src/onboarding/api.ts +32 -0
- package/src/onboarding/service.ts +149 -0
- package/src/onboarding/steps.ts +75 -0
- package/src/onboarding/waivers.ts +40 -0
- package/src/options.ts +534 -34
- package/src/page-presentation.ts +47 -2
- package/src/profile/api.ts +175 -0
- package/src/profile/links.ts +162 -0
- package/src/push/api.ts +189 -52
- package/src/push/dispatch.ts +156 -0
- package/src/push/drain.ts +139 -0
- package/src/push/outbox.ts +218 -0
- package/src/push/prefs.ts +118 -0
- package/src/push/sender.ts +7 -2
- package/src/push/triggers.ts +56 -5
- package/src/push/vapid.ts +19 -0
- package/src/push/web-push-transport.ts +106 -0
- package/src/push-sender/index.ts +14 -0
- package/src/push-sender/run.ts +118 -0
- package/src/push-sender/session.ts +54 -0
- package/src/pwa.ts +32 -4
- package/src/visitors.ts +94 -6
- package/src/web-app.ts +718 -0
- package/src/web-plugin.ts +279 -26
- package/dist/chunk-action-menu-qe839g4b.js +0 -1
- package/dist/chunk-arranged-rooms-enpfswmg.js +0 -1
- package/dist/chunk-character-creator-6yf29xwx.js +0 -1
- package/dist/chunk-codeeditor-gdbdg7rp.js +0 -2
- package/dist/chunk-confirm-xrw7d732.js +0 -1
- package/dist/chunk-conversations-ry228aes.js +0 -1
- package/dist/chunk-folder-list-5cp8stna.js +0 -1
- package/dist/chunk-folds-hff104tk.js +0 -1
- package/dist/chunk-group-scene-h228jdat.js +0 -1
- package/dist/chunk-index-0p5h2kdz.js +0 -2
- package/dist/chunk-index-0pt08yd9.js +0 -1
- package/dist/chunk-index-12bw9yj8.js +0 -2
- package/dist/chunk-index-2ad17eh4.js +0 -2
- package/dist/chunk-index-31wh5twj.js +0 -52
- package/dist/chunk-index-4dy94pbz.js +0 -2
- package/dist/chunk-index-4ecjtdy8.js +0 -13
- package/dist/chunk-index-4v1b7ks9.js +0 -2
- package/dist/chunk-index-5d8yjj4m.js +0 -2
- package/dist/chunk-index-89ajmxh7.js +0 -2
- package/dist/chunk-index-b9ny4623.js +0 -2
- package/dist/chunk-index-e1dr6g10.js +0 -2
- package/dist/chunk-index-gpkzhq1g.js +0 -2
- package/dist/chunk-index-hk3xyy20.js +0 -2
- package/dist/chunk-index-jm23vn03.js +0 -4
- package/dist/chunk-index-k7gh0jvz.js +0 -2
- package/dist/chunk-index-ky42ev0b.js +0 -2
- package/dist/chunk-index-pa2yyqh0.js +0 -2
- package/dist/chunk-index-pcvt668q.js +0 -2
- package/dist/chunk-index-pqw27eye.js +0 -1
- package/dist/chunk-index-wmbmmrd4.js +0 -302
- package/dist/chunk-index-ymbjrsp6.js +0 -2
- package/dist/chunk-lang-css-t85r8rz0.js +0 -1
- package/dist/chunk-lang-html-n6jparge.js +0 -1
- package/dist/chunk-lang-javascript-xj00cp97.js +0 -1
- package/dist/chunk-lang-markdown-zxx8eke8.js +0 -6
- package/dist/chunk-lang-typescript-8f6yywt3.js +0 -1
- package/dist/chunk-layout-actions-bv0chj41.js +0 -1
- package/dist/chunk-notes-9b7pgqfe.js +0 -1
- package/dist/chunk-overview-8yw71esz.js +0 -1
- package/dist/chunk-palette-frms7axe.js +0 -1
- package/dist/chunk-placement-items-fvfy9m0y.js +0 -1
- package/dist/chunk-prompt-history-yrqehads.js +0 -1
- package/dist/chunk-prompts-g6609tqx.js +0 -1
- package/dist/chunk-room-confirms-0a2y0925.js +0 -1
- package/dist/chunk-room-menu-g11q97br.js +0 -1
- package/dist/chunk-rooms-menu-button-h9qge48n.js +0 -1
- package/dist/chunk-schedules-ezmacrbj.js +0 -1
- package/dist/chunk-sheet-hosts-b0azyfhb.js +0 -1
- package/dist/chunk-skill-editor-h6k6whwg.js +0 -8
- package/dist/chunk-skills-gqcfhq56.js +0 -1
- package/dist/chunk-switcher-list-mcpamqnj.js +0 -1
- package/dist/chunk-table-s8cde1xz.js +0 -1
- package/dist/chunk-table-seats-wqkjn8bc.js +0 -1
- package/dist/chunk-transcript-c59c6mpm.js +0 -1
- package/dist/chunk-workspace-0cyspkk4.js +0 -1
- package/dist/chunk-zh-tw-nc93jm10.js +0 -1
package/src/app/protocol.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { QUESTIONNAIRE_LIMITS } from "pi-roundtable";
|
|
2
2
|
import {
|
|
3
3
|
type ClientFrame,
|
|
4
|
+
type FormInput,
|
|
4
5
|
MAX_MESSAGE_FILES,
|
|
5
6
|
type PromptAnswer,
|
|
6
7
|
} from "../api-types.ts";
|
|
@@ -40,6 +41,31 @@ function parseAnswer(value: unknown): PromptAnswer | undefined {
|
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/** The most fields and the longest value a form's input may carry. */
|
|
45
|
+
const FORM_FIELDS = 40;
|
|
46
|
+
const FORM_VALUE_CHARS = 32_000;
|
|
47
|
+
|
|
48
|
+
/** A form's input: text, or a list of text, by field name. */
|
|
49
|
+
function parseInput(value: unknown): FormInput | undefined {
|
|
50
|
+
if (!isRecord(value)) return undefined;
|
|
51
|
+
const entries = Object.entries(value);
|
|
52
|
+
if (entries.length > FORM_FIELDS) return undefined;
|
|
53
|
+
const input: Record<string, string | string[]> = {};
|
|
54
|
+
for (const [name, field] of entries) {
|
|
55
|
+
if (name.length === 0 || name.length > ID_CHARS) return undefined;
|
|
56
|
+
if (typeof field === "string" && field.length <= FORM_VALUE_CHARS)
|
|
57
|
+
input[name] = field;
|
|
58
|
+
else if (
|
|
59
|
+
Array.isArray(field) &&
|
|
60
|
+
field.length <= FORM_FIELDS &&
|
|
61
|
+
field.every((one) => isText(one) && one.length <= FORM_VALUE_CHARS)
|
|
62
|
+
)
|
|
63
|
+
input[name] = field as string[];
|
|
64
|
+
else return undefined;
|
|
65
|
+
}
|
|
66
|
+
return input;
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
function parseAnswers(value: Record<string, unknown>): ClientFrame | undefined {
|
|
44
70
|
const { prompt, answers, note } = value;
|
|
45
71
|
if (!isId(prompt) || !Array.isArray(answers)) return undefined;
|
|
@@ -99,8 +125,10 @@ export function parseClientFrame(
|
|
|
99
125
|
...(value.room === undefined ? {} : { room: value.room as string }),
|
|
100
126
|
};
|
|
101
127
|
case "send": {
|
|
102
|
-
const { id, room, text, uploads } = value;
|
|
128
|
+
const { id, room, text, uploads, mode } = value;
|
|
103
129
|
if (!isId(id) || !isId(room) || !isText(text)) return undefined;
|
|
130
|
+
if (mode !== undefined && mode !== "note" && mode !== "turn")
|
|
131
|
+
return undefined;
|
|
104
132
|
if (
|
|
105
133
|
uploads !== undefined &&
|
|
106
134
|
!(
|
|
@@ -118,8 +146,42 @@ export function parseClientFrame(
|
|
|
118
146
|
...(uploads === undefined || uploads.length === 0
|
|
119
147
|
? {}
|
|
120
148
|
: { uploads: uploads as string[] }),
|
|
149
|
+
...(mode === undefined ? {} : { mode }),
|
|
121
150
|
};
|
|
122
151
|
}
|
|
152
|
+
case "subscribe":
|
|
153
|
+
case "unsubscribe":
|
|
154
|
+
return isId(value.room)
|
|
155
|
+
? { type: value.type, room: value.room }
|
|
156
|
+
: undefined;
|
|
157
|
+
case "act": {
|
|
158
|
+
const { id, room, page, action, card, input } = value;
|
|
159
|
+
if (!isId(id) || !isId(action)) return undefined;
|
|
160
|
+
// One target: a room or a page, never both and never neither.
|
|
161
|
+
if ((room === undefined) === (page === undefined)) return undefined;
|
|
162
|
+
if (!optional(room, isId) || !optional(page, isId)) return undefined;
|
|
163
|
+
if (!optional(card, isId)) return undefined;
|
|
164
|
+
const parsed = input === undefined ? undefined : parseInput(input);
|
|
165
|
+
if (input !== undefined && !parsed) return undefined;
|
|
166
|
+
return {
|
|
167
|
+
type: "act",
|
|
168
|
+
id,
|
|
169
|
+
action,
|
|
170
|
+
...(room === undefined ? {} : { room: room as string }),
|
|
171
|
+
...(page === undefined ? {} : { page: page as string }),
|
|
172
|
+
...(card === undefined ? {} : { card: card as string }),
|
|
173
|
+
...(parsed ? { input: parsed } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
case "ack":
|
|
177
|
+
return isId(value.tag) ? { type: "ack", tag: value.tag } : undefined;
|
|
178
|
+
case "read":
|
|
179
|
+
return isId(value.room) &&
|
|
180
|
+
typeof value.upTo === "number" &&
|
|
181
|
+
Number.isSafeInteger(value.upTo) &&
|
|
182
|
+
value.upTo >= 0
|
|
183
|
+
? { type: "read", room: value.room, upTo: value.upTo }
|
|
184
|
+
: undefined;
|
|
123
185
|
case "stop":
|
|
124
186
|
return isId(value.room) ? { type: "stop", room: value.room } : undefined;
|
|
125
187
|
case "approval":
|
package/src/app/room-log.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
2
|
import type { Migration } from "pi-roundtable";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
HostAuthorKind,
|
|
5
|
+
PictureRef,
|
|
6
|
+
RoomFile,
|
|
7
|
+
RoomMessage,
|
|
8
|
+
} from "../api-types.ts";
|
|
9
|
+
import { storedJson } from "../host/stored-json.ts";
|
|
4
10
|
|
|
5
11
|
/** A message to store; the log numbers it and stamps its time. */
|
|
6
12
|
export interface NewRoomMessage {
|
|
@@ -11,6 +17,16 @@ export interface NewRoomMessage {
|
|
|
11
17
|
author: string;
|
|
12
18
|
text: string;
|
|
13
19
|
files?: RoomFile[];
|
|
20
|
+
/** Host rooms: who wrote it, as the host's kinds of author name them. */
|
|
21
|
+
authorKind?: HostAuthorKind;
|
|
22
|
+
authorLabel?: string;
|
|
23
|
+
picture?: PictureRef;
|
|
24
|
+
/** Host rooms: the host's own id for the message, unique within the room. */
|
|
25
|
+
reference?: string;
|
|
26
|
+
/** When it was written, for an import; the log's clock otherwise. Retention counts from it. */
|
|
27
|
+
at?: Date;
|
|
28
|
+
/** An internal note: shown to the room's readers, never handed to the agent. */
|
|
29
|
+
note?: true;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
interface Row {
|
|
@@ -23,16 +39,18 @@ interface Row {
|
|
|
23
39
|
text: string;
|
|
24
40
|
files: RoomFile[] | string;
|
|
25
41
|
created_at: Date;
|
|
42
|
+
author_kind: HostAuthorKind | null;
|
|
43
|
+
author_label: string | null;
|
|
44
|
+
picture: PictureRef | string | null;
|
|
45
|
+
edited_at: Date | null;
|
|
46
|
+
note: boolean | null;
|
|
26
47
|
}
|
|
27
48
|
|
|
28
49
|
/** The most messages one page of history holds. */
|
|
29
50
|
export const PAGE_LIMIT = 100;
|
|
30
51
|
|
|
31
52
|
function messageOf(row: Row): RoomMessage {
|
|
32
|
-
const files =
|
|
33
|
-
typeof row.files === "string"
|
|
34
|
-
? (JSON.parse(row.files) as RoomFile[])
|
|
35
|
-
: row.files;
|
|
53
|
+
const files = storedJson(row.files, "message files");
|
|
36
54
|
return {
|
|
37
55
|
id: Number(row.id),
|
|
38
56
|
room: row.room,
|
|
@@ -43,6 +61,15 @@ function messageOf(row: Row): RoomMessage {
|
|
|
43
61
|
text: row.text,
|
|
44
62
|
files,
|
|
45
63
|
at: row.created_at.toISOString(),
|
|
64
|
+
...(row.author_kind === null ? {} : { authorKind: row.author_kind }),
|
|
65
|
+
...(row.author_label === null ? {} : { authorLabel: row.author_label }),
|
|
66
|
+
...(row.picture === null
|
|
67
|
+
? {}
|
|
68
|
+
: {
|
|
69
|
+
picture: storedJson(row.picture, "picture"),
|
|
70
|
+
}),
|
|
71
|
+
...(row.edited_at === null ? {} : { edited: true as const }),
|
|
72
|
+
...(row.note ? { note: true as const } : {}),
|
|
46
73
|
};
|
|
47
74
|
}
|
|
48
75
|
|
|
@@ -79,29 +106,141 @@ export class RoomLog {
|
|
|
79
106
|
},
|
|
80
107
|
};
|
|
81
108
|
|
|
109
|
+
/** The columns a host room's messages add: who wrote it, the host's reference, and when it was changed. */
|
|
110
|
+
static readonly hostMigration: Migration = {
|
|
111
|
+
name: "web-room-messages-host",
|
|
112
|
+
up: async (sql) => {
|
|
113
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS author_kind text`;
|
|
114
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS author_label text`;
|
|
115
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS picture jsonb`;
|
|
116
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS reference text`;
|
|
117
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS edited_at timestamptz`;
|
|
118
|
+
await sql`CREATE UNIQUE INDEX IF NOT EXISTS web_room_messages_reference ON web_room_messages (room, reference) WHERE reference IS NOT NULL`;
|
|
119
|
+
await sql`CREATE INDEX IF NOT EXISTS web_room_messages_created ON web_room_messages (created_at)`;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** The column that marks a message as an internal note, for the rooms more than one person reads. */
|
|
124
|
+
static readonly noteMigration: Migration = {
|
|
125
|
+
name: "web-room-messages-note",
|
|
126
|
+
up: async (sql) => {
|
|
127
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS note boolean NOT NULL DEFAULT false`;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
82
131
|
async append(message: NewRoomMessage): Promise<RoomMessage> {
|
|
83
|
-
const
|
|
84
|
-
const [row]: Row[] = await this.#sql`
|
|
85
|
-
INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files)
|
|
86
|
-
VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
|
|
87
|
-
${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb)
|
|
88
|
-
RETURNING *`;
|
|
132
|
+
const [row] = await this.#insert(message, false);
|
|
89
133
|
if (!row) throw new Error("the room log stored nothing");
|
|
90
134
|
return messageOf(row);
|
|
91
135
|
}
|
|
92
136
|
|
|
93
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Stores a host room's message once per `reference`: a second post with the same reference in
|
|
139
|
+
* the room stores nothing and answers the message already there, with `created` false.
|
|
140
|
+
*/
|
|
141
|
+
async appendOnce(
|
|
142
|
+
message: NewRoomMessage,
|
|
143
|
+
): Promise<{ message: RoomMessage; created: boolean }> {
|
|
144
|
+
const [row] = await this.#insert(message, message.reference !== undefined);
|
|
145
|
+
if (row) return { message: messageOf(row), created: true };
|
|
146
|
+
const [existing]: Row[] = await this.#sql`
|
|
147
|
+
SELECT * FROM web_room_messages
|
|
148
|
+
WHERE room = ${message.room} AND reference = ${message.reference ?? null}`;
|
|
149
|
+
if (!existing) throw new Error("the room log stored nothing");
|
|
150
|
+
return { message: messageOf(existing), created: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async #insert(message: NewRoomMessage, once: boolean): Promise<Row[]> {
|
|
154
|
+
const files = JSON.stringify(message.files ?? []);
|
|
155
|
+
const picture = message.picture ? JSON.stringify(message.picture) : null;
|
|
156
|
+
const at = message.at ?? new Date();
|
|
157
|
+
return once
|
|
158
|
+
? await this.#sql`
|
|
159
|
+
INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
|
|
160
|
+
author_kind, author_label, picture, reference, created_at, note)
|
|
161
|
+
VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
|
|
162
|
+
${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
|
|
163
|
+
${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
|
|
164
|
+
${message.reference ?? null}, ${at}, ${message.note === true})
|
|
165
|
+
ON CONFLICT (room, reference) WHERE reference IS NOT NULL DO NOTHING
|
|
166
|
+
RETURNING *`
|
|
167
|
+
: await this.#sql`
|
|
168
|
+
INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
|
|
169
|
+
author_kind, author_label, picture, reference, created_at, note)
|
|
170
|
+
VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
|
|
171
|
+
${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
|
|
172
|
+
${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
|
|
173
|
+
${message.reference ?? null}, ${at}, ${message.note === true})
|
|
174
|
+
RETURNING *`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Changes the text of a message stored in the room; undefined when the room holds no such
|
|
179
|
+
* message. `mark` shows it as edited, as a host's edit of someone's words does.
|
|
180
|
+
*/
|
|
94
181
|
async edit(
|
|
95
182
|
room: string,
|
|
96
183
|
id: number,
|
|
97
184
|
text: string,
|
|
185
|
+
mark = false,
|
|
98
186
|
): Promise<RoomMessage | undefined> {
|
|
187
|
+
const [row]: Row[] = mark
|
|
188
|
+
? await this.#sql`
|
|
189
|
+
UPDATE web_room_messages SET text = ${text}, edited_at = now()
|
|
190
|
+
WHERE id = ${id} AND room = ${room} RETURNING *`
|
|
191
|
+
: await this.#sql`
|
|
192
|
+
UPDATE web_room_messages SET text = ${text}
|
|
193
|
+
WHERE id = ${id} AND room = ${room} RETURNING *`;
|
|
194
|
+
return row && messageOf(row);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The message the host's reference names, if the room holds one. */
|
|
198
|
+
async byReference(
|
|
199
|
+
room: string,
|
|
200
|
+
reference: string,
|
|
201
|
+
): Promise<number | undefined> {
|
|
202
|
+
const [row]: { id: string | number | bigint }[] = await this.#sql`
|
|
203
|
+
SELECT id FROM web_room_messages WHERE room = ${room} AND reference = ${reference}`;
|
|
204
|
+
return row === undefined ? undefined : Number(row.id);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One stored message of the room, or undefined. */
|
|
208
|
+
async get(room: string, id: number): Promise<RoomMessage | undefined> {
|
|
209
|
+
const [row]: Row[] = await this.#sql`
|
|
210
|
+
SELECT * FROM web_room_messages WHERE room = ${room} AND id = ${id}`;
|
|
211
|
+
return row && messageOf(row);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Deletes one message of the room; the message as it was, or undefined when the room held none. */
|
|
215
|
+
async remove(room: string, id: number): Promise<RoomMessage | undefined> {
|
|
99
216
|
const [row]: Row[] = await this.#sql`
|
|
100
|
-
|
|
101
|
-
WHERE id = ${id} AND room = ${room} RETURNING *`;
|
|
217
|
+
DELETE FROM web_room_messages WHERE room = ${room} AND id = ${id} RETURNING *`;
|
|
102
218
|
return row && messageOf(row);
|
|
103
219
|
}
|
|
104
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Deletes the messages older than `before` of the rooms whose key starts with `prefix`, the
|
|
223
|
+
* kind's rooms; the deleted messages, so their files can go too. Age is the message's own time.
|
|
224
|
+
*/
|
|
225
|
+
async sweepKind(prefix: string, before: Date): Promise<RoomMessage[]> {
|
|
226
|
+
const rows: Row[] = await this.#sql`
|
|
227
|
+
DELETE FROM web_room_messages
|
|
228
|
+
WHERE starts_with(room, ${prefix}) AND created_at < ${before} RETURNING *`;
|
|
229
|
+
return rows.map(messageOf);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The newest message id of each room that has one, for the read cursors. */
|
|
233
|
+
async newestIds(rooms: readonly string[]): Promise<Map<string, number>> {
|
|
234
|
+
const found = new Map<string, number>();
|
|
235
|
+
if (rooms.length === 0) return found;
|
|
236
|
+
const rows: { room: string; id: string | number | bigint }[] = await this
|
|
237
|
+
.#sql`
|
|
238
|
+
SELECT room, max(id) AS id FROM web_room_messages
|
|
239
|
+
WHERE room IN ${this.#sql(rooms)} GROUP BY room`;
|
|
240
|
+
for (const row of rows) found.set(row.room, Number(row.id));
|
|
241
|
+
return found;
|
|
242
|
+
}
|
|
243
|
+
|
|
105
244
|
/** The newest `limit` messages older than `before` (all of them when absent), oldest first. */
|
|
106
245
|
async page(
|
|
107
246
|
room: string,
|
package/src/app/rooms.ts
CHANGED
|
@@ -45,6 +45,11 @@ export interface RoomsOptions {
|
|
|
45
45
|
activity(
|
|
46
46
|
room: ChannelKey,
|
|
47
47
|
): Pick<RoomView, "typing" | "stoppable" | "phase" | "agent">;
|
|
48
|
+
/**
|
|
49
|
+
* Whether more than one person reads the agent and group rooms, so a message may be an
|
|
50
|
+
* internal note. Absent: one person does, and a room takes only turns.
|
|
51
|
+
*/
|
|
52
|
+
readers?(): Promise<boolean>;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
const THREAD_ID = /^thread\.[0-9a-f-]{36}$/;
|
|
@@ -111,6 +116,11 @@ export class Rooms {
|
|
|
111
116
|
: undefined;
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
/** Whether the agent and group rooms take internal notes: more than one person reads them. */
|
|
120
|
+
async notesOn(): Promise<boolean> {
|
|
121
|
+
return (await this.#options.readers?.()) === true;
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
/** The home room: the coordinator's, else the first active agent's. */
|
|
115
125
|
homeKey(): ChannelKey | undefined {
|
|
116
126
|
const { directory, team, surface } = this.#options;
|
|
@@ -153,6 +163,33 @@ export class Rooms {
|
|
|
153
163
|
return `${this.#options.mount}${path ?? "/avatars/missing.png"}`;
|
|
154
164
|
}
|
|
155
165
|
|
|
166
|
+
/**
|
|
167
|
+
* The view of one thread, listed or not: a thread the owners dismissed is off the list and still
|
|
168
|
+
* opens by its address, and the page asks for its view here.
|
|
169
|
+
*/
|
|
170
|
+
async threadView(key: ChannelKey): Promise<RoomView | undefined> {
|
|
171
|
+
const room = await this.resolve(key);
|
|
172
|
+
if (room?.kind !== "thread") return undefined;
|
|
173
|
+
const thread = await this.#options.threads.get(room.name);
|
|
174
|
+
if (!thread) return undefined;
|
|
175
|
+
const { modes, activity, log } = this.#options;
|
|
176
|
+
const { yolo, longRunning } = modes.peek(key);
|
|
177
|
+
const last = (await log.lastOf([key])).get(key);
|
|
178
|
+
return {
|
|
179
|
+
home: false,
|
|
180
|
+
readOnly: true,
|
|
181
|
+
mode: { yolo, longRunning },
|
|
182
|
+
...activity(key),
|
|
183
|
+
kind: "thread",
|
|
184
|
+
name: thread.id,
|
|
185
|
+
displayName: thread.title,
|
|
186
|
+
parent: thread.parent,
|
|
187
|
+
...(thread.open ? {} : { closed: true as const }),
|
|
188
|
+
...(last ? { last } : {}),
|
|
189
|
+
key,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
156
193
|
async list(): Promise<RoomView[]> {
|
|
157
194
|
const { surface, directory, modes, mutes, activity, log, threads } =
|
|
158
195
|
this.#options;
|
|
@@ -162,6 +199,7 @@ export class Rooms {
|
|
|
162
199
|
const groups = directory.groups().filter((g) => g.status === "active");
|
|
163
200
|
const shown = await threads.list();
|
|
164
201
|
const muted = await mutes.all();
|
|
202
|
+
const notes = await this.notesOn();
|
|
165
203
|
const view = (
|
|
166
204
|
key: ChannelKey,
|
|
167
205
|
rest: Pick<RoomView, "kind" | "name" | "displayName"> & Partial<RoomView>,
|
|
@@ -172,6 +210,7 @@ export class Rooms {
|
|
|
172
210
|
readOnly: false,
|
|
173
211
|
mode: { yolo, longRunning },
|
|
174
212
|
...(muted.has(key) ? { muted: true as const } : {}),
|
|
213
|
+
...(notes && rest.kind !== "thread" ? { notes: true as const } : {}),
|
|
175
214
|
...activity(key),
|
|
176
215
|
...rest,
|
|
177
216
|
key,
|