pi-roundtable-web 0.24.2 → 0.26.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/CHANGELOG.md +48 -0
- package/README.md +195 -16
- package/dist/account-zk4efb87.css +102 -0
- package/dist/chunk-account-hw49gd0d.js +1 -0
- package/dist/chunk-action-menu-rm5cyqkj.js +1 -0
- package/dist/chunk-agent-b017vswv.js +1 -0
- package/dist/chunk-arranged-rooms-q5v3dm1r.js +1 -0
- package/dist/chunk-character-creator-5dxcvd16.js +1 -0
- package/dist/chunk-codeeditor-pyt09fwg.js +2 -0
- package/dist/chunk-confirm-521yqmta.js +1 -0
- package/dist/chunk-connectors-hrm06nx8.js +1 -0
- package/dist/chunk-conversations-e26rrf01.js +1 -0
- package/dist/chunk-drag-v41gx2mc.js +1 -0
- package/dist/chunk-entry-jxxb3ath.js +1 -0
- package/dist/chunk-folder-list-b97xk12b.js +1 -0
- package/dist/chunk-folds-b8q7eags.js +1 -0
- package/dist/chunk-gate-8k61stsc.js +1 -0
- package/dist/chunk-group-scene-dx6wt19m.js +1 -0
- package/dist/chunk-group-table-e4jr8r3a.js +1 -0
- package/dist/chunk-host-state-g7e7c0k5.js +1 -0
- package/dist/chunk-host-ui-x7k6n07v.js +939 -0
- package/dist/chunk-index-014rjv9k.js +2 -0
- package/dist/chunk-index-2d0kg096.js +2 -0
- package/dist/chunk-index-2qd81wbx.js +2 -0
- package/dist/chunk-index-2stynxw6.css +1 -0
- package/dist/chunk-index-459drsft.js +2 -0
- package/dist/chunk-index-4mfj596y.js +2 -0
- package/dist/chunk-index-66nskrdj.js +47 -0
- package/dist/chunk-index-awx6nnta.js +2 -0
- package/dist/chunk-index-bbsytycb.js +2 -0
- package/dist/chunk-index-c07p8k0x.js +4 -0
- package/dist/chunk-index-cq1e9xdz.js +2 -0
- package/dist/chunk-index-d1prgfbp.js +2 -0
- package/dist/chunk-index-dhpt8qr9.js +2 -0
- package/dist/chunk-index-e5sd30xy.js +2 -0
- package/dist/chunk-index-g351921r.js +1 -0
- package/dist/chunk-index-hby0ap91.js +2 -0
- package/dist/chunk-index-hssjbr1k.js +2 -0
- package/dist/chunk-index-k2qq1gdj.js +2 -0
- package/dist/chunk-index-ke9ynm0n.js +2 -0
- package/dist/chunk-index-km40bhsz.js +2 -0
- package/dist/chunk-index-kpscvg85.js +2 -0
- package/dist/chunk-index-m0d100mg.js +2 -0
- package/dist/chunk-index-mb72snk9.js +2 -0
- package/dist/chunk-index-mjqqver5.js +4 -0
- package/dist/chunk-index-nqfkwkfp.js +2 -0
- package/dist/chunk-index-nwqk447w.js +2 -0
- package/dist/chunk-index-pcfhpm8v.js +302 -0
- package/dist/chunk-index-q8h8xzbm.js +2 -0
- package/dist/chunk-index-r5vszgfs.js +2 -0
- package/dist/{chunk-index-mpm90318.js → chunk-index-rhtvcbsq.js} +2 -2
- package/dist/chunk-index-rjv7vrwd.js +13 -0
- package/dist/chunk-index-v23m38p6.js +50 -0
- package/dist/chunk-index-va0g7d88.js +1 -0
- package/dist/{chunk-index-6t68gbex.js → chunk-index-wnjp6wv0.js} +2 -2
- package/dist/chunk-index-xgkd15n3.js +2 -0
- package/dist/chunk-index-y8rnq8zf.js +14 -0
- package/dist/chunk-index-z3k18atc.js +2 -0
- package/dist/chunk-lang-css-6bepqpnx.js +1 -0
- package/dist/{chunk-lang-dockerfile-ey0nvhhm.js → chunk-lang-dockerfile-hrav55t7.js} +1 -1
- package/dist/{chunk-lang-go-403qsw9f.js → chunk-lang-go-x9ahv6e5.js} +3 -3
- package/dist/chunk-lang-html-8jc72zt4.js +1 -0
- package/dist/chunk-lang-javascript-2p2qtbnf.js +1 -0
- package/dist/chunk-lang-json-hsemyd0r.js +1 -0
- package/dist/chunk-lang-markdown-zkwb4c9n.js +6 -0
- package/dist/{chunk-lang-python-9jk5a49y.js → chunk-lang-python-tewx7006.js} +2 -2
- package/dist/chunk-lang-rust-0ydx84s1.js +1 -0
- package/dist/{chunk-lang-shell-cgq6q0kj.js → chunk-lang-shell-gq1tmdz4.js} +1 -1
- package/dist/chunk-lang-sql-xt1qyycg.js +2 -0
- package/dist/{chunk-lang-toml-xh45nm68.js → chunk-lang-toml-0hamc5fc.js} +1 -1
- package/dist/chunk-lang-typescript-82xf7z2e.js +1 -0
- package/dist/{chunk-lang-xml-1mh879j9.js → chunk-lang-xml-wf6h5pt2.js} +1 -1
- package/dist/{chunk-lang-yaml-jac98srt.js → chunk-lang-yaml-7vk521h1.js} +1 -1
- package/dist/chunk-layout-actions-zh7bezk7.js +1 -0
- package/dist/chunk-menu-items-1n9qargb.js +2 -0
- package/dist/chunk-notes-t15bh04m.js +1 -0
- package/dist/chunk-notice-bell-w3y0h0sr.js +1 -0
- package/dist/chunk-notices-fa3g7qze.js +1 -0
- package/dist/chunk-onboarding-34v3nr17.js +1 -0
- package/dist/chunk-overview-km5vq375.js +1 -0
- package/dist/chunk-palette-a8txdgd0.js +1 -0
- package/dist/chunk-placement-items-wgaxsbvs.js +1 -0
- package/dist/chunk-profile-ypxdbsk1.js +1 -0
- package/dist/chunk-prompt-history-wm9dsy6s.js +1 -0
- package/dist/chunk-prompts-7mhhq7zs.js +1 -0
- package/dist/chunk-room-confirms-t11a1gsw.js +1 -0
- package/dist/chunk-room-menu-2tm2xtfx.js +1 -0
- package/dist/chunk-rooms-menu-button-wxnsxjwc.js +1 -0
- package/dist/chunk-schedules-2ft6xxyw.js +1 -0
- package/dist/chunk-seat-menu-d3npdtx1.js +1 -0
- package/dist/chunk-sheet-hosts-vngvp7wv.js +1 -0
- package/dist/chunk-skill-editor-65n2hsq4.js +8 -0
- package/dist/chunk-skills-shkdwes8.js +1 -0
- package/dist/chunk-switcher-list-g6hhmr1t.js +1 -0
- package/dist/chunk-table-1b6bsq2y.js +1 -0
- package/dist/chunk-table-seats-vy1qhdks.js +1 -0
- package/dist/chunk-table2d-n26b7gd0.js +1 -0
- package/dist/chunk-table3d-8aysyf56.js +4220 -0
- package/dist/chunk-transcript-355faceq.js +1 -0
- package/dist/chunk-where-j5qdt8a4.js +1 -0
- package/dist/chunk-workspace-vrbs6zx1.js +1 -0
- package/dist/chunk-zh-tw-zeg600bk.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 +515 -6
- package/src/app/api-http.ts +66 -0
- package/src/app/app.ts +214 -33
- package/src/app/card-log.ts +6 -0
- package/src/app/desk.ts +47 -22
- package/src/app/files.ts +73 -7
- package/src/app/layout-routes.ts +239 -0
- package/src/app/layout.ts +133 -0
- package/src/app/presence.ts +2 -2
- package/src/app/prompt-routes.ts +167 -0
- package/src/app/protocol.ts +63 -1
- package/src/app/room-log.ts +148 -14
- package/src/app/rooms.ts +23 -0
- package/src/app/routes.ts +278 -156
- package/src/app/skill-doc-routes.ts +122 -0
- package/src/app/socket.ts +309 -77
- package/src/app/sockets.ts +108 -20
- package/src/app/surface.ts +72 -36
- package/src/app/team-routes.ts +457 -0
- package/src/app/threads.ts +14 -0
- package/src/app/uploads.ts +33 -7
- package/src/auth/authenticator.ts +71 -8
- package/src/auth/next.ts +52 -0
- package/src/auth/oidc-pages.ts +60 -0
- package/src/auth/oidc.ts +611 -0
- package/src/auth/sessions.ts +193 -0
- package/src/authz.ts +172 -0
- package/src/console-api.ts +122 -25
- package/src/console-server.ts +70 -6
- package/src/gate.ts +90 -0
- package/src/host/access.ts +161 -0
- package/src/host/cards.ts +99 -0
- package/src/host/context.ts +141 -0
- package/src/host/forms.ts +237 -0
- package/src/host/frames.ts +342 -0
- package/src/host/host-app.ts +176 -0
- package/src/host/host-context.ts +49 -0
- package/src/host/host-routes.ts +363 -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 +414 -0
- package/src/host/stored-json.ts +13 -0
- package/src/host/views.ts +103 -0
- package/src/i18n/zh-tw.ts +309 -10
- package/src/layout-order.ts +210 -0
- 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 +498 -34
- 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/mutes.ts +17 -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/visitors.ts +94 -6
- package/src/web-app.ts +674 -0
- package/src/web-plugin.ts +293 -26
- package/dist/chunk-action-menu-7v5fce2v.js +0 -1
- package/dist/chunk-agent-2rw4ydw2.js +0 -1
- package/dist/chunk-agent-sheet-2q24dkkf.js +0 -3
- package/dist/chunk-character-creator-8q6ympxj.js +0 -1
- package/dist/chunk-codeeditor-fgmq298t.js +0 -2
- package/dist/chunk-connectors-mr53r7fh.js +0 -1
- package/dist/chunk-conversations-4vy2z6w9.js +0 -1
- package/dist/chunk-entry-jg1eem82.js +0 -1
- package/dist/chunk-group-scene-na95b1te.js +0 -1
- package/dist/chunk-index-11rjg8a8.js +0 -1
- package/dist/chunk-index-1y50d6sj.js +0 -2
- package/dist/chunk-index-5y2r0eaj.js +0 -47
- package/dist/chunk-index-7fphxxzd.js +0 -14
- package/dist/chunk-index-80f1zkp6.css +0 -1
- package/dist/chunk-index-87h45frg.js +0 -2
- package/dist/chunk-index-bnwmssyn.js +0 -2
- package/dist/chunk-index-ckgbszwt.js +0 -51
- package/dist/chunk-index-fmjjekhd.js +0 -2
- package/dist/chunk-index-t1m65yn6.js +0 -13
- package/dist/chunk-index-t711t17m.js +0 -2
- package/dist/chunk-index-txv7qvyy.js +0 -2
- package/dist/chunk-index-wr3dg24d.js +0 -2
- package/dist/chunk-index-yx683d5n.js +0 -2
- package/dist/chunk-lang-css-fkdg3x8x.js +0 -1
- package/dist/chunk-lang-html-v8h9g977.js +0 -1
- package/dist/chunk-lang-javascript-ykb2vhaa.js +0 -1
- package/dist/chunk-lang-json-2rze3rpy.js +0 -1
- package/dist/chunk-lang-markdown-6w2htnfc.js +0 -6
- package/dist/chunk-lang-rust-24fggcqx.js +0 -1
- package/dist/chunk-lang-sql-gjywb9ke.js +0 -2
- package/dist/chunk-lang-typescript-qh6rebyb.js +0 -1
- package/dist/chunk-menu-items-zjpyj51b.js +0 -2
- package/dist/chunk-notes-sjba7kw3.js +0 -1
- package/dist/chunk-overview-26r77kb6.js +0 -1
- package/dist/chunk-room-confirms-851184ep.js +0 -1
- package/dist/chunk-room-menu-8y9c026y.js +0 -1
- package/dist/chunk-skills-56s4hdbn.js +0 -1
- package/dist/chunk-table-qykpvp32.js +0 -1
- package/dist/chunk-table2d-k60ressj.js +0 -1
- package/dist/chunk-table3d-bv2ajk39.js +0 -4220
- package/dist/chunk-transcript-vekz1mh6.js +0 -1
- package/dist/chunk-workspace-hevefd0d.js +0 -1
- package/dist/chunk-zh-tw-5r2wjre0.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,14 @@ 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;
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
interface Row {
|
|
@@ -23,16 +37,17 @@ interface Row {
|
|
|
23
37
|
text: string;
|
|
24
38
|
files: RoomFile[] | string;
|
|
25
39
|
created_at: Date;
|
|
40
|
+
author_kind: HostAuthorKind | null;
|
|
41
|
+
author_label: string | null;
|
|
42
|
+
picture: PictureRef | string | null;
|
|
43
|
+
edited_at: Date | null;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/** The most messages one page of history holds. */
|
|
29
47
|
export const PAGE_LIMIT = 100;
|
|
30
48
|
|
|
31
49
|
function messageOf(row: Row): RoomMessage {
|
|
32
|
-
const files =
|
|
33
|
-
typeof row.files === "string"
|
|
34
|
-
? (JSON.parse(row.files) as RoomFile[])
|
|
35
|
-
: row.files;
|
|
50
|
+
const files = storedJson(row.files, "message files");
|
|
36
51
|
return {
|
|
37
52
|
id: Number(row.id),
|
|
38
53
|
room: row.room,
|
|
@@ -43,6 +58,14 @@ function messageOf(row: Row): RoomMessage {
|
|
|
43
58
|
text: row.text,
|
|
44
59
|
files,
|
|
45
60
|
at: row.created_at.toISOString(),
|
|
61
|
+
...(row.author_kind === null ? {} : { authorKind: row.author_kind }),
|
|
62
|
+
...(row.author_label === null ? {} : { authorLabel: row.author_label }),
|
|
63
|
+
...(row.picture === null
|
|
64
|
+
? {}
|
|
65
|
+
: {
|
|
66
|
+
picture: storedJson(row.picture, "picture"),
|
|
67
|
+
}),
|
|
68
|
+
...(row.edited_at === null ? {} : { edited: true as const }),
|
|
46
69
|
};
|
|
47
70
|
}
|
|
48
71
|
|
|
@@ -79,29 +102,133 @@ export class RoomLog {
|
|
|
79
102
|
},
|
|
80
103
|
};
|
|
81
104
|
|
|
105
|
+
/** The columns a host room's messages add: who wrote it, the host's reference, and when it was changed. */
|
|
106
|
+
static readonly hostMigration: Migration = {
|
|
107
|
+
name: "web-room-messages-host",
|
|
108
|
+
up: async (sql) => {
|
|
109
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS author_kind text`;
|
|
110
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS author_label text`;
|
|
111
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS picture jsonb`;
|
|
112
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS reference text`;
|
|
113
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS edited_at timestamptz`;
|
|
114
|
+
await sql`CREATE UNIQUE INDEX IF NOT EXISTS web_room_messages_reference ON web_room_messages (room, reference) WHERE reference IS NOT NULL`;
|
|
115
|
+
await sql`CREATE INDEX IF NOT EXISTS web_room_messages_created ON web_room_messages (created_at)`;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
82
119
|
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 *`;
|
|
120
|
+
const [row] = await this.#insert(message, false);
|
|
89
121
|
if (!row) throw new Error("the room log stored nothing");
|
|
90
122
|
return messageOf(row);
|
|
91
123
|
}
|
|
92
124
|
|
|
93
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Stores a host room's message once per `reference`: a second post with the same reference in
|
|
127
|
+
* the room stores nothing and answers the message already there, with `created` false.
|
|
128
|
+
*/
|
|
129
|
+
async appendOnce(
|
|
130
|
+
message: NewRoomMessage,
|
|
131
|
+
): Promise<{ message: RoomMessage; created: boolean }> {
|
|
132
|
+
const [row] = await this.#insert(message, message.reference !== undefined);
|
|
133
|
+
if (row) return { message: messageOf(row), created: true };
|
|
134
|
+
const [existing]: Row[] = await this.#sql`
|
|
135
|
+
SELECT * FROM web_room_messages
|
|
136
|
+
WHERE room = ${message.room} AND reference = ${message.reference ?? null}`;
|
|
137
|
+
if (!existing) throw new Error("the room log stored nothing");
|
|
138
|
+
return { message: messageOf(existing), created: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async #insert(message: NewRoomMessage, once: boolean): Promise<Row[]> {
|
|
142
|
+
const files = JSON.stringify(message.files ?? []);
|
|
143
|
+
const picture = message.picture ? JSON.stringify(message.picture) : null;
|
|
144
|
+
const at = message.at ?? new Date();
|
|
145
|
+
return once
|
|
146
|
+
? await this.#sql`
|
|
147
|
+
INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
|
|
148
|
+
author_kind, author_label, picture, reference, created_at)
|
|
149
|
+
VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
|
|
150
|
+
${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
|
|
151
|
+
${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
|
|
152
|
+
${message.reference ?? null}, ${at})
|
|
153
|
+
ON CONFLICT (room, reference) WHERE reference IS NOT NULL DO NOTHING
|
|
154
|
+
RETURNING *`
|
|
155
|
+
: await this.#sql`
|
|
156
|
+
INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
|
|
157
|
+
author_kind, author_label, picture, reference, created_at)
|
|
158
|
+
VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
|
|
159
|
+
${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
|
|
160
|
+
${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
|
|
161
|
+
${message.reference ?? null}, ${at})
|
|
162
|
+
RETURNING *`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Changes the text of a message stored in the room; undefined when the room holds no such
|
|
167
|
+
* message. `mark` shows it as edited, as a host's edit of someone's words does.
|
|
168
|
+
*/
|
|
94
169
|
async edit(
|
|
95
170
|
room: string,
|
|
96
171
|
id: number,
|
|
97
172
|
text: string,
|
|
173
|
+
mark = false,
|
|
98
174
|
): Promise<RoomMessage | undefined> {
|
|
175
|
+
const [row]: Row[] = mark
|
|
176
|
+
? await this.#sql`
|
|
177
|
+
UPDATE web_room_messages SET text = ${text}, edited_at = now()
|
|
178
|
+
WHERE id = ${id} AND room = ${room} RETURNING *`
|
|
179
|
+
: await this.#sql`
|
|
180
|
+
UPDATE web_room_messages SET text = ${text}
|
|
181
|
+
WHERE id = ${id} AND room = ${room} RETURNING *`;
|
|
182
|
+
return row && messageOf(row);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The message the host's reference names, if the room holds one. */
|
|
186
|
+
async byReference(
|
|
187
|
+
room: string,
|
|
188
|
+
reference: string,
|
|
189
|
+
): Promise<number | undefined> {
|
|
190
|
+
const [row]: { id: string | number | bigint }[] = await this.#sql`
|
|
191
|
+
SELECT id FROM web_room_messages WHERE room = ${room} AND reference = ${reference}`;
|
|
192
|
+
return row === undefined ? undefined : Number(row.id);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** One stored message of the room, or undefined. */
|
|
196
|
+
async get(room: string, id: number): Promise<RoomMessage | undefined> {
|
|
197
|
+
const [row]: Row[] = await this.#sql`
|
|
198
|
+
SELECT * FROM web_room_messages WHERE room = ${room} AND id = ${id}`;
|
|
199
|
+
return row && messageOf(row);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Deletes one message of the room; the message as it was, or undefined when the room held none. */
|
|
203
|
+
async remove(room: string, id: number): Promise<RoomMessage | undefined> {
|
|
99
204
|
const [row]: Row[] = await this.#sql`
|
|
100
|
-
|
|
101
|
-
WHERE id = ${id} AND room = ${room} RETURNING *`;
|
|
205
|
+
DELETE FROM web_room_messages WHERE room = ${room} AND id = ${id} RETURNING *`;
|
|
102
206
|
return row && messageOf(row);
|
|
103
207
|
}
|
|
104
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Deletes the messages older than `before` of the rooms whose key starts with `prefix`, the
|
|
211
|
+
* kind's rooms; the deleted messages, so their files can go too. Age is the message's own time.
|
|
212
|
+
*/
|
|
213
|
+
async sweepKind(prefix: string, before: Date): Promise<RoomMessage[]> {
|
|
214
|
+
const rows: Row[] = await this.#sql`
|
|
215
|
+
DELETE FROM web_room_messages
|
|
216
|
+
WHERE starts_with(room, ${prefix}) AND created_at < ${before} RETURNING *`;
|
|
217
|
+
return rows.map(messageOf);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The newest message id of each room that has one, for the read cursors. */
|
|
221
|
+
async newestIds(rooms: readonly string[]): Promise<Map<string, number>> {
|
|
222
|
+
const found = new Map<string, number>();
|
|
223
|
+
if (rooms.length === 0) return found;
|
|
224
|
+
const rows: { room: string; id: string | number | bigint }[] = await this
|
|
225
|
+
.#sql`
|
|
226
|
+
SELECT room, max(id) AS id FROM web_room_messages
|
|
227
|
+
WHERE room IN ${this.#sql(rooms)} GROUP BY room`;
|
|
228
|
+
for (const row of rows) found.set(row.room, Number(row.id));
|
|
229
|
+
return found;
|
|
230
|
+
}
|
|
231
|
+
|
|
105
232
|
/** The newest `limit` messages older than `before` (all of them when absent), oldest first. */
|
|
106
233
|
async page(
|
|
107
234
|
room: string,
|
|
@@ -123,6 +250,13 @@ export class RoomLog {
|
|
|
123
250
|
};
|
|
124
251
|
}
|
|
125
252
|
|
|
253
|
+
/** Deletes every message of a room that is gone for good; returns how many. */
|
|
254
|
+
async removeRoom(room: string): Promise<number> {
|
|
255
|
+
const rows: unknown[] = await this
|
|
256
|
+
.#sql`DELETE FROM web_room_messages WHERE room = ${room} RETURNING 1`;
|
|
257
|
+
return rows.length;
|
|
258
|
+
}
|
|
259
|
+
|
|
126
260
|
/** The newest message of each of the rooms that has one. */
|
|
127
261
|
async lastOf(rooms: readonly string[]): Promise<Map<string, RoomMessage>> {
|
|
128
262
|
const found = new Map<string, RoomMessage>();
|
package/src/app/rooms.ts
CHANGED
|
@@ -123,6 +123,27 @@ export class Rooms {
|
|
|
123
123
|
: undefined;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The keys of the rooms an owner can pin or put in a folder: the active agents' and groups' rooms,
|
|
128
|
+
* without Home, which stays at the top, and without threads, which follow their room.
|
|
129
|
+
*/
|
|
130
|
+
placeable(): Set<string> {
|
|
131
|
+
const { surface, directory } = this.#options;
|
|
132
|
+
const home = this.homeKey();
|
|
133
|
+
return new Set<string>(
|
|
134
|
+
[
|
|
135
|
+
...directory
|
|
136
|
+
.agents()
|
|
137
|
+
.filter((agent) => agent.status === "active")
|
|
138
|
+
.map((agent) => agentRoomKey(surface, agent.name)),
|
|
139
|
+
...directory
|
|
140
|
+
.groups()
|
|
141
|
+
.filter((group) => group.status === "active")
|
|
142
|
+
.map((group) => groupRoomKey(surface, group.name)),
|
|
143
|
+
].filter((key) => key !== home),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
126
147
|
/** The agent whose avatar path to show, as a path under the mount. */
|
|
127
148
|
avatarOf(agent: Agent): string {
|
|
128
149
|
// The studio gives a full URL with a public address and a bare path without one.
|
|
@@ -136,6 +157,7 @@ export class Rooms {
|
|
|
136
157
|
const { surface, directory, modes, mutes, activity, log, threads } =
|
|
137
158
|
this.#options;
|
|
138
159
|
const home = this.homeKey();
|
|
160
|
+
const coordinator = this.#options.team.coordinator();
|
|
139
161
|
const agents = directory.agents().filter((a) => a.status === "active");
|
|
140
162
|
const groups = directory.groups().filter((g) => g.status === "active");
|
|
141
163
|
const shown = await threads.list();
|
|
@@ -163,6 +185,7 @@ export class Rooms {
|
|
|
163
185
|
displayName: agent.displayName,
|
|
164
186
|
avatar: this.avatarOf(agent),
|
|
165
187
|
character: characterOf(agent),
|
|
188
|
+
...(agent.name === coordinator ? { coordinator: true as const } : {}),
|
|
166
189
|
}),
|
|
167
190
|
),
|
|
168
191
|
...groups.map((group: AgentGroup) =>
|