pi-roundtable-web 0.26.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 +39 -0
- package/README.md +23 -7
- package/dist/chunk-account-ghq105t4.js +1 -0
- package/dist/{chunk-action-menu-rm5cyqkj.js → chunk-action-menu-me0g9qsc.js} +1 -1
- package/dist/{chunk-agent-b017vswv.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-hrm06nx8.js → chunk-connectors-bkv3xn83.js} +1 -1
- package/dist/{chunk-conversations-e26rrf01.js → chunk-conversations-0e0vbxwr.js} +1 -1
- package/dist/{chunk-drag-v41gx2mc.js → chunk-drag-peyb8t0k.js} +1 -1
- package/dist/{chunk-entry-jxxb3ath.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-8k61stsc.js → chunk-gate-hc7dd0fx.js} +1 -1
- 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-2d0kg096.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-d1prgfbp.js → chunk-index-b6ca02e8.js} +2 -2
- package/dist/chunk-index-b8nvhcx5.js +2 -0
- package/dist/{chunk-index-014rjv9k.js → chunk-index-cvbdf2ca.js} +2 -2
- package/dist/chunk-index-czdketze.js +2 -0
- package/dist/{chunk-index-y8rnq8zf.js → chunk-index-dt9775ka.js} +3 -3
- package/dist/chunk-index-eg55mqqe.js +1 -0
- package/dist/{chunk-index-xgkd15n3.js → chunk-index-gr6e0hse.js} +1 -1
- 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-wnjp6wv0.js → chunk-index-k7t14rwa.js} +2 -2
- package/dist/{chunk-index-c07p8k0x.js → chunk-index-kwcx62wb.js} +3 -3
- package/dist/{chunk-index-hby0ap91.js → chunk-index-m7fvw92f.js} +2 -2
- 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-rhtvcbsq.js → chunk-index-pk1yxkv4.js} +2 -2
- package/dist/{chunk-index-2stynxw6.css → chunk-index-rbcz9jd4.css} +1 -1
- package/dist/{chunk-index-km40bhsz.js → chunk-index-rmxbdmn2.js} +2 -2
- package/dist/chunk-index-rnxj3z8k.js +2 -0
- package/dist/{chunk-index-hssjbr1k.js → chunk-index-snzzfhk3.js} +2 -2
- package/dist/chunk-index-v3bmr0nw.js +2 -0
- package/dist/{chunk-index-awx6nnta.js → chunk-index-vcyzwtk2.js} +1 -1
- package/dist/{chunk-index-nqfkwkfp.js → chunk-index-w2j3njb6.js} +2 -2
- package/dist/{chunk-index-e5sd30xy.js → chunk-index-w31c4yjv.js} +2 -2
- 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-dhpt8qr9.js → chunk-index-yzsgsvnp.js} +2 -2
- package/dist/{chunk-index-66nskrdj.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-hrav55t7.js → chunk-lang-dockerfile-k728ewsf.js} +1 -1
- package/dist/{chunk-lang-go-x9ahv6e5.js → chunk-lang-go-wjwgay4v.js} +2 -2
- package/dist/chunk-lang-html-07d47es4.js +1 -0
- package/dist/chunk-lang-javascript-fmhhgzwd.js +1 -0
- package/dist/{chunk-lang-json-hsemyd0r.js → chunk-lang-json-7gd89mvs.js} +1 -1
- package/dist/{chunk-lang-markdown-zkwb4c9n.js → chunk-lang-markdown-b6qbe8bt.js} +5 -5
- package/dist/{chunk-lang-python-tewx7006.js → chunk-lang-python-3v56y31b.js} +2 -2
- package/dist/{chunk-lang-rust-0ydx84s1.js → chunk-lang-rust-kh0z74d1.js} +1 -1
- package/dist/{chunk-lang-shell-gq1tmdz4.js → chunk-lang-shell-9zv0zvbx.js} +1 -1
- package/dist/{chunk-lang-sql-xt1qyycg.js → chunk-lang-sql-yw6bf3t7.js} +2 -2
- package/dist/{chunk-lang-toml-0hamc5fc.js → chunk-lang-toml-tg1r96nq.js} +1 -1
- package/dist/chunk-lang-typescript-etjjvr2g.js +1 -0
- package/dist/{chunk-lang-xml-wf6h5pt2.js → chunk-lang-xml-ev1mndd4.js} +1 -1
- package/dist/{chunk-lang-yaml-7vk521h1.js → chunk-lang-yaml-fz5qpb3r.js} +1 -1
- package/dist/chunk-layout-actions-48ecgtkg.js +1 -0
- package/dist/{chunk-menu-items-1n9qargb.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-t15bh04m.js → chunk-notes-yb8f057c.js} +1 -1
- 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-km5vq375.js → chunk-overview-avc2d30t.js} +1 -1
- package/dist/{chunk-palette-a8txdgd0.js → chunk-palette-9nhp95bz.js} +1 -1
- package/dist/chunk-placement-items-q694dpgk.js +1 -0
- package/dist/chunk-profile-6qemf3m7.js +1 -0
- package/dist/{chunk-prompt-history-wm9dsy6s.js → chunk-prompt-history-7enzp2p3.js} +1 -1
- package/dist/chunk-prompts-nv6mrn0q.js +1 -0
- package/dist/chunk-room-confirms-zz7n61y7.js +1 -0
- package/dist/{chunk-room-menu-2tm2xtfx.js → chunk-room-menu-rr6nzap1.js} +1 -1
- package/dist/{chunk-rooms-menu-button-wxnsxjwc.js → chunk-rooms-menu-button-zbtwj47d.js} +1 -1
- package/dist/chunk-schedules-rw57nr0m.js +1 -0
- package/dist/{chunk-seat-menu-d3npdtx1.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-shkdwes8.js → chunk-skills-tnr1h16h.js} +1 -1
- 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-n26b7gd0.js → chunk-table2d-p3ezsdcj.js} +1 -1
- package/dist/{chunk-table3d-8aysyf56.js → chunk-table3d-4x2n2b00.js} +20 -20
- 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 +2 -2
- package/src/api-types.ts +58 -3
- package/src/app/app.ts +5 -0
- package/src/app/room-log.ts +16 -4
- package/src/app/rooms.ts +39 -0
- package/src/app/routes.ts +44 -2
- package/src/app/socket.ts +45 -9
- package/src/app/threads.ts +31 -2
- package/src/auth/authenticator.ts +10 -4
- package/src/auth/login-page.ts +4 -2
- package/src/auth/oidc-pages.ts +2 -0
- package/src/auth/oidc.ts +39 -1
- package/src/authz.ts +2 -1
- package/src/console-server.ts +32 -6
- package/src/host/cards.ts +18 -5
- package/src/host/context.ts +72 -25
- package/src/host/forms.ts +3 -0
- package/src/host/frames.ts +8 -1
- package/src/host/host-app.ts +6 -4
- package/src/host/host-routes.ts +36 -3
- package/src/host/room-views.ts +6 -6
- package/src/host/rooms-service.ts +16 -1
- package/src/host/views.ts +3 -0
- package/src/i18n/zh-tw.ts +9 -2
- package/src/index.ts +15 -1
- package/src/options.ts +36 -0
- package/src/page-presentation.ts +47 -2
- package/src/pwa.ts +32 -4
- package/src/web-app.ts +47 -3
- package/src/web-plugin.ts +20 -9
- package/dist/chunk-account-hw49gd0d.js +0 -1
- package/dist/chunk-arranged-rooms-q5v3dm1r.js +0 -1
- package/dist/chunk-character-creator-5dxcvd16.js +0 -1
- package/dist/chunk-codeeditor-pyt09fwg.js +0 -2
- package/dist/chunk-confirm-521yqmta.js +0 -1
- package/dist/chunk-folder-list-b97xk12b.js +0 -1
- package/dist/chunk-folds-b8q7eags.js +0 -1
- package/dist/chunk-group-scene-dx6wt19m.js +0 -1
- package/dist/chunk-group-table-e4jr8r3a.js +0 -1
- package/dist/chunk-host-state-g7e7c0k5.js +0 -1
- package/dist/chunk-host-ui-x7k6n07v.js +0 -939
- package/dist/chunk-index-2qd81wbx.js +0 -2
- package/dist/chunk-index-459drsft.js +0 -2
- package/dist/chunk-index-4mfj596y.js +0 -2
- package/dist/chunk-index-bbsytycb.js +0 -2
- package/dist/chunk-index-cq1e9xdz.js +0 -2
- package/dist/chunk-index-g351921r.js +0 -1
- package/dist/chunk-index-k2qq1gdj.js +0 -2
- package/dist/chunk-index-ke9ynm0n.js +0 -2
- package/dist/chunk-index-kpscvg85.js +0 -2
- package/dist/chunk-index-m0d100mg.js +0 -2
- package/dist/chunk-index-mb72snk9.js +0 -2
- package/dist/chunk-index-mjqqver5.js +0 -4
- package/dist/chunk-index-nwqk447w.js +0 -2
- package/dist/chunk-index-pcfhpm8v.js +0 -302
- package/dist/chunk-index-q8h8xzbm.js +0 -2
- package/dist/chunk-index-r5vszgfs.js +0 -2
- package/dist/chunk-index-rjv7vrwd.js +0 -13
- package/dist/chunk-index-v23m38p6.js +0 -50
- package/dist/chunk-index-va0g7d88.js +0 -1
- package/dist/chunk-index-z3k18atc.js +0 -2
- package/dist/chunk-lang-css-6bepqpnx.js +0 -1
- package/dist/chunk-lang-html-8jc72zt4.js +0 -1
- package/dist/chunk-lang-javascript-2p2qtbnf.js +0 -1
- package/dist/chunk-lang-typescript-82xf7z2e.js +0 -1
- package/dist/chunk-layout-actions-zh7bezk7.js +0 -1
- package/dist/chunk-notice-bell-w3y0h0sr.js +0 -1
- package/dist/chunk-notices-fa3g7qze.js +0 -1
- package/dist/chunk-onboarding-34v3nr17.js +0 -1
- package/dist/chunk-placement-items-wgaxsbvs.js +0 -1
- package/dist/chunk-profile-ypxdbsk1.js +0 -1
- package/dist/chunk-prompts-7mhhq7zs.js +0 -1
- package/dist/chunk-room-confirms-t11a1gsw.js +0 -1
- package/dist/chunk-schedules-2ft6xxyw.js +0 -1
- package/dist/chunk-sheet-hosts-vngvp7wv.js +0 -1
- package/dist/chunk-skill-editor-65n2hsq4.js +0 -8
- package/dist/chunk-switcher-list-g6hhmr1t.js +0 -1
- package/dist/chunk-table-1b6bsq2y.js +0 -1
- package/dist/chunk-table-seats-vy1qhdks.js +0 -1
- package/dist/chunk-transcript-355faceq.js +0 -1
- package/dist/chunk-where-j5qdt8a4.js +0 -1
- package/dist/chunk-workspace-vrbs6zx1.js +0 -1
- package/dist/chunk-zh-tw-zeg600bk.js +0 -1
package/src/host/forms.ts
CHANGED
|
@@ -125,6 +125,7 @@ export function checkCard(card: {
|
|
|
125
125
|
state?: string | undefined;
|
|
126
126
|
links?: readonly { label: string; url: string }[] | undefined;
|
|
127
127
|
buttons?: readonly HostAction[] | undefined;
|
|
128
|
+
keep?: boolean | undefined;
|
|
128
129
|
}): void {
|
|
129
130
|
if (typeof card.title !== "string" || card.title.length > LIMITS.title)
|
|
130
131
|
fail(`a card's title is at most ${LIMITS.title} characters`);
|
|
@@ -134,6 +135,8 @@ export function checkCard(card: {
|
|
|
134
135
|
fail(`a card's tone is note, ok, warn or risk`);
|
|
135
136
|
if (card.state !== undefined && !STATES.has(card.state))
|
|
136
137
|
fail(`a card's state is open, working, done, superseded or expired`);
|
|
138
|
+
if (card.keep !== undefined && typeof card.keep !== "boolean")
|
|
139
|
+
fail("a card's keep is true or false");
|
|
137
140
|
const links = card.links ?? [];
|
|
138
141
|
if (links.length > LIMITS.links)
|
|
139
142
|
fail(`a card takes at most ${LIMITS.links} links`);
|
package/src/host/frames.ts
CHANGED
|
@@ -60,7 +60,7 @@ export async function actInRoom(
|
|
|
60
60
|
if (press.card !== undefined) {
|
|
61
61
|
const card = await ctx.cards.get(room, press.card);
|
|
62
62
|
if (!card) return { refused: "unknown" };
|
|
63
|
-
offered = buttonsFor(viewer, found, access, card);
|
|
63
|
+
offered = await buttonsFor(viewer, found, access, card);
|
|
64
64
|
} else {
|
|
65
65
|
const header = await found.kind
|
|
66
66
|
.header?.(viewer, found.id)
|
|
@@ -146,6 +146,13 @@ export function installFrames(ctx: HostContext): void {
|
|
|
146
146
|
connection,
|
|
147
147
|
audience: { room: frame.room },
|
|
148
148
|
});
|
|
149
|
+
// So is a turn already running: a seat at the round table shows it from the first frame, not after the next change.
|
|
150
|
+
const activity = ctx.app.activityOf(asRoom(frame.room));
|
|
151
|
+
if (activity.typing)
|
|
152
|
+
ctx.sockets.deliver(
|
|
153
|
+
{ type: "activity", room: frame.room, ...activity },
|
|
154
|
+
{ connection, audience: { room: frame.room } },
|
|
155
|
+
);
|
|
149
156
|
},
|
|
150
157
|
);
|
|
151
158
|
|
package/src/host/host-app.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { Uploads } from "../app/uploads.ts";
|
|
|
19
19
|
import type { Viewer, WebAppRegistry } from "../web-app.ts";
|
|
20
20
|
import { HostAccess } from "./access.ts";
|
|
21
21
|
import type { HostCardLog } from "./cards.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { type HostRoomContext, hostRoomContext } from "./context.ts";
|
|
23
23
|
import { installFrames } from "./frames.ts";
|
|
24
24
|
import type { HostContext } from "./host-context.ts";
|
|
25
25
|
import { hostArea, pagesArea } from "./host-routes.ts";
|
|
@@ -66,7 +66,7 @@ export class HostApp {
|
|
|
66
66
|
readonly rooms: HostRoomsService;
|
|
67
67
|
readonly pages: HostPagesService;
|
|
68
68
|
readonly #context: HostContext;
|
|
69
|
-
readonly #
|
|
69
|
+
readonly #roomContext: HostRoomContext;
|
|
70
70
|
|
|
71
71
|
constructor(options: HostAppOptions) {
|
|
72
72
|
const now = options.now ?? Date.now;
|
|
@@ -102,6 +102,8 @@ export class HostApp {
|
|
|
102
102
|
maxFileBytes: options.maxFileBytes,
|
|
103
103
|
roomUrl: options.roomUrl,
|
|
104
104
|
listed,
|
|
105
|
+
context: (room, contextOptions) =>
|
|
106
|
+
this.#roomContext.peek(room, contextOptions),
|
|
105
107
|
now,
|
|
106
108
|
logger: options.logger,
|
|
107
109
|
});
|
|
@@ -126,7 +128,7 @@ export class HostApp {
|
|
|
126
128
|
now,
|
|
127
129
|
logger: options.logger,
|
|
128
130
|
};
|
|
129
|
-
this.#
|
|
131
|
+
this.#roomContext = hostRoomContext(this.#context);
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
/** Plugs the pieces into the registry: who reads, the frames, the areas, and the services a host reaches. */
|
|
@@ -153,7 +155,7 @@ export class HostApp {
|
|
|
153
155
|
message: InboundMessage,
|
|
154
156
|
options?: ChannelContextOptions,
|
|
155
157
|
): Promise<ChannelContext | undefined> {
|
|
156
|
-
return this.#
|
|
158
|
+
return this.#roomContext.read(message, options);
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
/** Whether a stored file bound to a room or a page may be served to this viewer now. */
|
package/src/host/host-routes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HostCardView, RoomMessage } from "../api-types.ts";
|
|
1
|
+
import type { HostCardView, RoomMessage, SendWording } from "../api-types.ts";
|
|
2
2
|
import { bad, bodyOf, HttpError } from "../app/api-http.ts";
|
|
3
3
|
import { PAGE_LIMIT } from "../app/room-log.ts";
|
|
4
4
|
import { json } from "../http.ts";
|
|
@@ -244,14 +244,15 @@ async function open(
|
|
|
244
244
|
ctx.log.page(room, undefined, OPEN_LIMIT),
|
|
245
245
|
ctx.cards.list(room),
|
|
246
246
|
]);
|
|
247
|
-
const cards: HostCardView[] =
|
|
248
|
-
cardView(viewer, found, access, card),
|
|
247
|
+
const cards: HostCardView[] = await Promise.all(
|
|
248
|
+
stored.map((card) => cardView(viewer, found, access, card)),
|
|
249
249
|
);
|
|
250
250
|
return {
|
|
251
251
|
room,
|
|
252
252
|
kind: found.kind.kind,
|
|
253
253
|
id: found.id,
|
|
254
254
|
access: { send: [...access.send], act: access.act },
|
|
255
|
+
...wordingOf(found.kind.sendWording),
|
|
255
256
|
header,
|
|
256
257
|
messages: withExpiredFiles(page.messages, ctx.files),
|
|
257
258
|
more: page.more,
|
|
@@ -261,6 +262,38 @@ async function open(
|
|
|
261
262
|
};
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
/** The longest label and placeholder a kind may give, in characters. */
|
|
266
|
+
const LABEL_CHARS = 40;
|
|
267
|
+
const PLACEHOLDER_CHARS = 100;
|
|
268
|
+
|
|
269
|
+
const trimmed = (value: unknown, chars: number): string | undefined =>
|
|
270
|
+
typeof value === "string"
|
|
271
|
+
? Array.from(value.trim()).slice(0, chars).join("") || undefined
|
|
272
|
+
: undefined;
|
|
273
|
+
|
|
274
|
+
/** The kind's wording of the two ways to write as the page may show it: text only, cut short; nothing when it gave none. */
|
|
275
|
+
function wordingOf(wording: SendWording | undefined): {
|
|
276
|
+
wording?: SendWording;
|
|
277
|
+
} {
|
|
278
|
+
const note = trimmed(wording?.note?.placeholder, PLACEHOLDER_CHARS);
|
|
279
|
+
const label = trimmed(wording?.turn?.label, LABEL_CHARS);
|
|
280
|
+
const turn = trimmed(wording?.turn?.placeholder, PLACEHOLDER_CHARS);
|
|
281
|
+
if (!note && !label && !turn) return {};
|
|
282
|
+
return {
|
|
283
|
+
wording: {
|
|
284
|
+
...(note ? { note: { placeholder: note } } : {}),
|
|
285
|
+
...(label || turn
|
|
286
|
+
? {
|
|
287
|
+
turn: {
|
|
288
|
+
...(label ? { label } : {}),
|
|
289
|
+
...(turn ? { placeholder: turn } : {}),
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
: {}),
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
264
297
|
async function older(
|
|
265
298
|
ctx: HostContext,
|
|
266
299
|
viewer: Viewer,
|
package/src/host/room-views.ts
CHANGED
|
@@ -77,27 +77,27 @@ export interface FoundRoom {
|
|
|
77
77
|
* The buttons of a host card this viewer may press: none once the card is settled, none without
|
|
78
78
|
* `access.act`, and otherwise the card's own, narrowed by the kind's `buttons` when it has one.
|
|
79
79
|
*/
|
|
80
|
-
export function buttonsFor(
|
|
80
|
+
export async function buttonsFor(
|
|
81
81
|
viewer: Viewer,
|
|
82
82
|
found: FoundRoom,
|
|
83
83
|
access: RoomAccess,
|
|
84
84
|
card: StoredHostCard,
|
|
85
|
-
): NonNullable<StoredHostCard["buttons"]
|
|
85
|
+
): Promise<NonNullable<StoredHostCard["buttons"]>> {
|
|
86
86
|
const all = card.buttons ?? [];
|
|
87
87
|
if (!access.act || !pressable(card) || all.length === 0) return [];
|
|
88
88
|
if (!found.kind.buttons) return all;
|
|
89
|
-
const allowed = new Set(found.kind.buttons(viewer, found.id, card));
|
|
89
|
+
const allowed = new Set(await found.kind.buttons(viewer, found.id, card));
|
|
90
90
|
return all.filter((button) => allowed.has(button.id));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** A host card as one viewer sees it. */
|
|
94
|
-
export function cardView(
|
|
94
|
+
export async function cardView(
|
|
95
95
|
viewer: Viewer,
|
|
96
96
|
found: FoundRoom,
|
|
97
97
|
access: RoomAccess,
|
|
98
98
|
card: StoredHostCard,
|
|
99
|
-
): HostCardView {
|
|
100
|
-
const buttons = buttonsFor(viewer, found, access, card);
|
|
99
|
+
): Promise<HostCardView> {
|
|
100
|
+
const buttons = await buttonsFor(viewer, found, access, card);
|
|
101
101
|
return {
|
|
102
102
|
id: card.id,
|
|
103
103
|
room: card.room,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChannelKey, Logger } from "pi-roundtable";
|
|
2
2
|
import { PluginError } from "pi-roundtable";
|
|
3
|
+
import type { ChannelContext } from "pi-roundtable/discord";
|
|
3
4
|
import type { ServerFrame } from "../api-types.ts";
|
|
4
5
|
import { MAX_MESSAGE_CHARS } from "../api-types.ts";
|
|
5
6
|
import { type FileDir, isFileId, safeName } from "../app/files.ts";
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
} from "../web-app.ts";
|
|
16
17
|
import type { HostAccess } from "./access.ts";
|
|
17
18
|
import type { HostCardLog } from "./cards.ts";
|
|
19
|
+
import type { HostRoomContextOptions } from "./context.ts";
|
|
18
20
|
import { checkCard } from "./forms.ts";
|
|
19
21
|
import { cardView, checkPicture, type FoundRoom } from "./room-views.ts";
|
|
20
22
|
|
|
@@ -37,6 +39,11 @@ export interface HostRoomsOptions {
|
|
|
37
39
|
roomUrl(room: ChannelKey, message?: number): string;
|
|
38
40
|
/** Which kinds a person's last list showed, so the list's changes reach only them. */
|
|
39
41
|
listed: ListedKinds;
|
|
42
|
+
/** A room's recent messages as a channel-context block, without marking them handed over. */
|
|
43
|
+
context(
|
|
44
|
+
room: string,
|
|
45
|
+
options?: HostRoomContextOptions,
|
|
46
|
+
): Promise<ChannelContext | undefined>;
|
|
40
47
|
now?: () => number;
|
|
41
48
|
logger: Logger;
|
|
42
49
|
}
|
|
@@ -272,6 +279,14 @@ export class HostRoomsService implements HostRooms {
|
|
|
272
279
|
return this.#options.log.byReference(room, reference);
|
|
273
280
|
}
|
|
274
281
|
|
|
282
|
+
async context(
|
|
283
|
+
room: ChannelKey,
|
|
284
|
+
options?: HostRoomContextOptions,
|
|
285
|
+
): Promise<ChannelContext | undefined> {
|
|
286
|
+
this.#found(room);
|
|
287
|
+
return this.#options.context(room, options);
|
|
288
|
+
}
|
|
289
|
+
|
|
275
290
|
// ── Cards ──
|
|
276
291
|
|
|
277
292
|
async card(room: ChannelKey, card: HostCard): Promise<{ id: string }> {
|
|
@@ -313,7 +328,7 @@ export class HostRoomsService implements HostRooms {
|
|
|
313
328
|
{
|
|
314
329
|
type: "host_card",
|
|
315
330
|
room,
|
|
316
|
-
card: cardView(viewer, found, access, stored),
|
|
331
|
+
card: await cardView(viewer, found, access, stored),
|
|
317
332
|
},
|
|
318
333
|
{ connection, audience: { room } },
|
|
319
334
|
);
|
package/src/host/views.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
PictureRef,
|
|
9
9
|
ReadMarkView,
|
|
10
10
|
RoomMessage,
|
|
11
|
+
SendWording,
|
|
11
12
|
} from "../api-types.ts";
|
|
12
13
|
|
|
13
14
|
/** One room of a kind, as the list shows it. */
|
|
@@ -47,6 +48,8 @@ export interface HostRoomView {
|
|
|
47
48
|
id: string;
|
|
48
49
|
/** What this viewer may do in the room. */
|
|
49
50
|
access: { send: ("note" | "turn")[]; act: boolean };
|
|
51
|
+
/** The kind's own wording of the two ways to write, where it gave any. */
|
|
52
|
+
wording?: SendWording;
|
|
50
53
|
header: HostHeaderView;
|
|
51
54
|
/** The newest messages, oldest first. */
|
|
52
55
|
messages: RoomMessage[];
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -288,6 +288,7 @@ export const ZH_TW: Readonly<Record<string, string>> = {
|
|
|
288
288
|
"Leave the table": "離席",
|
|
289
289
|
"{name} left the table": "{name}已離席",
|
|
290
290
|
"The table is empty": "圓桌空了",
|
|
291
|
+
"There are no rooms to seat yet.": "還沒有可以入座的聊天室。",
|
|
291
292
|
"Every agent has left the table. Choose who sits down.":
|
|
292
293
|
"所有夥伴都離席了。選擇誰要上桌。",
|
|
293
294
|
"An agent who leaves the table is muted: its room stops pushing to you. A card that waits for you is always pushed.":
|
|
@@ -580,6 +581,8 @@ export const ZH_TW: Readonly<Record<string, string>> = {
|
|
|
580
581
|
"{count} cards waiting": "{count} 張卡片等待中",
|
|
581
582
|
"{count} finished thread": "{count} 個已結束的討論串",
|
|
582
583
|
"{count} finished threads": "{count} 個已結束的討論串",
|
|
584
|
+
"Clear all": "清除全部",
|
|
585
|
+
"The thread is still open.": "這個討論串還在進行中。",
|
|
583
586
|
|
|
584
587
|
// 排程
|
|
585
588
|
"{days} at {time}": "每{days} {time}",
|
|
@@ -1050,8 +1053,12 @@ export const ZH_TW: Readonly<Record<string, string>> = {
|
|
|
1050
1053
|
"This page is not available.": "這個頁面無法使用。",
|
|
1051
1054
|
"You can read this room but not write in it.":
|
|
1052
1055
|
"你可以閱讀這個聊天室,但不能在其中發言。",
|
|
1053
|
-
"
|
|
1054
|
-
"
|
|
1056
|
+
"@AI": "@AI",
|
|
1057
|
+
"The message starts with @AI, so the agent answers.":
|
|
1058
|
+
"訊息開頭是 @AI,所以 AI 會回覆。",
|
|
1059
|
+
"Write an internal note…": "寫一則內部備註…",
|
|
1060
|
+
"Ask AI…": "問 AI…",
|
|
1061
|
+
"Internal note": "內部備註",
|
|
1055
1062
|
"Earlier cards ({count})": "先前的卡片({count})",
|
|
1056
1063
|
Replaced: "已被取代",
|
|
1057
1064
|
Image: "圖片",
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type {
|
|
|
12
12
|
ConversationView,
|
|
13
13
|
DashboardView,
|
|
14
14
|
GroupView,
|
|
15
|
+
HostIconName,
|
|
15
16
|
NoteInput,
|
|
16
17
|
NoteKind,
|
|
17
18
|
NoteView,
|
|
@@ -26,7 +27,7 @@ export type {
|
|
|
26
27
|
TranscriptRole,
|
|
27
28
|
TranscriptView,
|
|
28
29
|
} from "./api-types.ts";
|
|
29
|
-
export { PANES } from "./api-types.ts";
|
|
30
|
+
export { HOST_ICONS, PANES } from "./api-types.ts";
|
|
30
31
|
export type { CloudflareAccessOptions } from "./cloudflare-access.ts";
|
|
31
32
|
export {
|
|
32
33
|
cloudflareAccess,
|
|
@@ -39,4 +40,17 @@ export type { WebAuthOptions, WebConsoleOptions } from "./options.ts";
|
|
|
39
40
|
export { DEFAULT_RELAY_NOTE } from "./options.ts";
|
|
40
41
|
export type { RequestVerifier, Verdict } from "./verifier.ts";
|
|
41
42
|
export { admit, admitAs, refuse } from "./verifier.ts";
|
|
43
|
+
export type {
|
|
44
|
+
ActionResult,
|
|
45
|
+
HostCard,
|
|
46
|
+
HostPage,
|
|
47
|
+
HostRoomKind,
|
|
48
|
+
LinkProver,
|
|
49
|
+
Notices,
|
|
50
|
+
OnboardingStep,
|
|
51
|
+
OpenTarget,
|
|
52
|
+
Viewer,
|
|
53
|
+
WebApp,
|
|
54
|
+
} from "./web-app.ts";
|
|
55
|
+
export { WEB_APP } from "./web-app.ts";
|
|
42
56
|
export { webConsole } from "./web-plugin.ts";
|
package/src/options.ts
CHANGED
|
@@ -116,6 +116,14 @@ export interface OidcOptions {
|
|
|
116
116
|
reauthHours?: number;
|
|
117
117
|
/** The id_token claims to keep with the session (replaced at each sign-in and re-verify, never logged); default none. */
|
|
118
118
|
storeClaims?: readonly string[];
|
|
119
|
+
/**
|
|
120
|
+
* Called with the person's principal id when a request or an open socket proves they hold a
|
|
121
|
+
* live session, at most once per session every five minutes. For a host that counts who is
|
|
122
|
+
* present, such as one that keeps a linked chat account only while its web person is active: a
|
|
123
|
+
* person who only has the app open, calling nothing of the host's, still counts. It is not
|
|
124
|
+
* awaited, and an error it throws is logged and never fails the request.
|
|
125
|
+
*/
|
|
126
|
+
onSessionVerified?: (principalId: string) => void | Promise<void>;
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
/** `auth` checked and filled in. */
|
|
@@ -145,6 +153,9 @@ export interface ResolvedOidc {
|
|
|
145
153
|
endSession: boolean;
|
|
146
154
|
reauthHours: number | undefined;
|
|
147
155
|
storeClaims: readonly string[];
|
|
156
|
+
onSessionVerified:
|
|
157
|
+
| ((principalId: string) => void | Promise<void>)
|
|
158
|
+
| undefined;
|
|
148
159
|
}
|
|
149
160
|
|
|
150
161
|
/** The panes each tier is served, when the list differs by tier. */
|
|
@@ -331,6 +342,11 @@ export interface WebConsoleOptions {
|
|
|
331
342
|
uploads?: { keepDays?: number };
|
|
332
343
|
/** The page's title and heading; default `Roundtable`. */
|
|
333
344
|
title?: string;
|
|
345
|
+
/**
|
|
346
|
+
* The page's description for search results and link previews (`description` and
|
|
347
|
+
* `og:description`); default a line derived from the title.
|
|
348
|
+
*/
|
|
349
|
+
description?: string;
|
|
334
350
|
/** Where the install icons come from; default the ones this package ships. */
|
|
335
351
|
icons?: { dir: string };
|
|
336
352
|
/** How the page looks: `modern` (default) or `camelot`. The page's owner can switch it in the app. */
|
|
@@ -397,6 +413,7 @@ export interface ResolvedOptions {
|
|
|
397
413
|
modes: boolean;
|
|
398
414
|
uploadKeepDays: number;
|
|
399
415
|
title: string;
|
|
416
|
+
description: string | undefined;
|
|
400
417
|
iconsDir: string | undefined;
|
|
401
418
|
look: "modern" | "camelot";
|
|
402
419
|
relayNotes: readonly string[];
|
|
@@ -546,6 +563,11 @@ function checkPanes(
|
|
|
546
563
|
`${label} names the ${pane} pane, which the owners' panes do not have; the pane's API is served for the owners' list`,
|
|
547
564
|
);
|
|
548
565
|
}
|
|
566
|
+
// A member's table seats the host rooms they may list, which the rooms pane brings with its socket.
|
|
567
|
+
if (list.includes("table") && !list.includes("rooms"))
|
|
568
|
+
fail(
|
|
569
|
+
`${label} names the table pane without the rooms pane, whose host rooms it seats`,
|
|
570
|
+
);
|
|
549
571
|
}
|
|
550
572
|
return { owner, admin, member, given: true };
|
|
551
573
|
}
|
|
@@ -704,6 +726,7 @@ function checkOidc(auth: OidcOptions): ResolvedOidc {
|
|
|
704
726
|
"endSession",
|
|
705
727
|
"reauthHours",
|
|
706
728
|
"storeClaims",
|
|
729
|
+
"onSessionVerified",
|
|
707
730
|
]);
|
|
708
731
|
for (const key of Object.keys(auth))
|
|
709
732
|
if (!known.has(key)) fail(`auth.${key} is not an oidc option`);
|
|
@@ -778,6 +801,11 @@ function checkOidc(auth: OidcOptions): ResolvedOidc {
|
|
|
778
801
|
!storeClaims.every((name) => typeof name === "string" && name.trim())
|
|
779
802
|
)
|
|
780
803
|
fail("auth.storeClaims must be a list of claim names");
|
|
804
|
+
if (
|
|
805
|
+
auth.onSessionVerified !== undefined &&
|
|
806
|
+
typeof auth.onSessionVerified !== "function"
|
|
807
|
+
)
|
|
808
|
+
fail("auth.onSessionVerified must be a function");
|
|
781
809
|
return {
|
|
782
810
|
mode: "oidc",
|
|
783
811
|
issuer,
|
|
@@ -798,6 +826,7 @@ function checkOidc(auth: OidcOptions): ResolvedOidc {
|
|
|
798
826
|
endSession: auth.endSession === true,
|
|
799
827
|
reauthHours: auth.reauthHours,
|
|
800
828
|
storeClaims: [...storeClaims],
|
|
829
|
+
onSessionVerified: auth.onSessionVerified,
|
|
801
830
|
};
|
|
802
831
|
}
|
|
803
832
|
|
|
@@ -1028,6 +1057,12 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
1028
1057
|
fail("appearance.look must be modern or camelot");
|
|
1029
1058
|
const title = options.title?.trim() ?? "Roundtable";
|
|
1030
1059
|
if (!title) fail("title is empty");
|
|
1060
|
+
const description = options.description?.trim();
|
|
1061
|
+
if (
|
|
1062
|
+
options.description !== undefined &&
|
|
1063
|
+
(typeof options.description !== "string" || !description)
|
|
1064
|
+
)
|
|
1065
|
+
fail("description is empty. Leave it out for the default");
|
|
1031
1066
|
return {
|
|
1032
1067
|
auth,
|
|
1033
1068
|
origin,
|
|
@@ -1050,6 +1085,7 @@ export function resolveOptions(options: WebConsoleOptions): ResolvedOptions {
|
|
|
1050
1085
|
modes: options.modes !== false,
|
|
1051
1086
|
uploadKeepDays: checkUploads(options),
|
|
1052
1087
|
title,
|
|
1088
|
+
description,
|
|
1053
1089
|
iconsDir: options.icons?.dir,
|
|
1054
1090
|
look,
|
|
1055
1091
|
relayNotes: options.relayNotes ?? [DEFAULT_RELAY_NOTE],
|
package/src/page-presentation.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ConsolePresentation } from "./features.ts";
|
|
|
2
2
|
import { escapeHtml } from "./http.ts";
|
|
3
3
|
import { PAGE_COLORS } from "./page-colors.ts";
|
|
4
4
|
|
|
5
|
-
/** The tags that make the page installable;
|
|
5
|
+
/** The tags that make the page installable; their URLs resolve against the base, so they follow it. */
|
|
6
6
|
const pwaTags = (look: "modern" | "camelot"): string =>
|
|
7
7
|
'<link rel="manifest" href="./manifest.webmanifest" crossorigin="use-credentials">' +
|
|
8
8
|
`<meta name="theme-color" content="${PAGE_COLORS[look].light}" media="(prefers-color-scheme: light)">` +
|
|
@@ -10,21 +10,66 @@ const pwaTags = (look: "modern" | "camelot"): string =>
|
|
|
10
10
|
'<link rel="apple-touch-icon" href="./icons/apple-180.png">' +
|
|
11
11
|
'<meta name="apple-mobile-web-app-capable" content="yes">';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The icon links of a page served under `mount`: the round-table mark as an SVG when the icon
|
|
15
|
+
* directory has one, and the 192 px PNG every icon directory has (the manifest names it). Without
|
|
16
|
+
* them a browser asks the site root for `/favicon.ico`, which is not this app's.
|
|
17
|
+
*/
|
|
18
|
+
export const iconLinks = (mount: string, svg: boolean): string => {
|
|
19
|
+
const base = escapeHtml(mount);
|
|
20
|
+
const mark = svg
|
|
21
|
+
? `<link rel="icon" href="${base}/icons/mark.svg" type="image/svg+xml" sizes="any">`
|
|
22
|
+
: "";
|
|
23
|
+
return `${mark}<link rel="icon" href="${base}/icons/192.png" type="image/png">`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** What a page says about itself to a tab, a search result and a link preview. */
|
|
27
|
+
export interface PageMeta {
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
/** The console's origin, so `og:image` is an absolute URL. */
|
|
31
|
+
origin: string;
|
|
32
|
+
/** The path the console is served under. */
|
|
33
|
+
mount: string;
|
|
34
|
+
/** Whether the icon directory has `mark.svg`. */
|
|
35
|
+
svg: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The text a host that sets no `description` gets. */
|
|
39
|
+
export const defaultDescription = (title: string): string =>
|
|
40
|
+
`Chat with your agent team in ${title}.`;
|
|
41
|
+
|
|
42
|
+
const metaTags = (meta: PageMeta): string =>
|
|
43
|
+
`<meta name="description" content="${escapeHtml(meta.description)}">` +
|
|
44
|
+
`<meta property="og:title" content="${escapeHtml(meta.title)}">` +
|
|
45
|
+
`<meta property="og:description" content="${escapeHtml(meta.description)}">` +
|
|
46
|
+
`<meta property="og:image" content="${escapeHtml(`${meta.origin}${meta.mount}/icons/512.png`)}">`;
|
|
47
|
+
|
|
13
48
|
/** Authenticated, inert bootstrap data localizes even a failed first API request without inline scripts. */
|
|
14
49
|
export function presentPage(
|
|
15
50
|
body: Uint8Array,
|
|
16
51
|
mount: string | undefined,
|
|
17
52
|
presentation: ConsolePresentation | undefined,
|
|
18
53
|
look: "modern" | "camelot" = "modern",
|
|
54
|
+
meta?: PageMeta,
|
|
19
55
|
): Uint8Array<ArrayBuffer> | string {
|
|
20
56
|
// Not in web/index.html: the bundler would try to resolve and rewrite these URLs.
|
|
21
|
-
|
|
57
|
+
// The base comes first: under path routing a relative link before it resolves against a deep URL.
|
|
58
|
+
let head = "<head>";
|
|
22
59
|
if (mount) head += `<base href="${escapeHtml(`${mount}/`)}">`;
|
|
60
|
+
head += pwaTags(look);
|
|
61
|
+
if (meta) head += iconLinks(meta.mount, meta.svg) + metaTags(meta);
|
|
23
62
|
if (presentation)
|
|
24
63
|
head += `<meta name="roundtable-console-presentation" content="${escapeHtml(JSON.stringify(presentation))}">`;
|
|
25
64
|
// After the base, so it resolves under the mount; a script of the page's own origin, which its policy allows.
|
|
26
65
|
head += '<script src="./theme-init.js"></script>';
|
|
27
66
|
let html = new TextDecoder().decode(body).replace("<head>", () => head);
|
|
67
|
+
// web/index.html ships a placeholder: the page is not rendered until the bundle sets the title.
|
|
68
|
+
if (meta)
|
|
69
|
+
html = html.replace(
|
|
70
|
+
/<title>[^<]*<\/title>/,
|
|
71
|
+
() => `<title>${escapeHtml(meta.title)}</title>`,
|
|
72
|
+
);
|
|
28
73
|
if (presentation?.locale)
|
|
29
74
|
html = html.replace(
|
|
30
75
|
/(<html[^>]*lang=")[^"]*(")/,
|
package/src/pwa.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { AssetBundle } from "./assets.ts";
|
|
@@ -34,7 +34,27 @@ export const DEFAULT_ICONS_DIR = fileURLToPath(
|
|
|
34
34
|
new URL("../assets/icons/", import.meta.url),
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
-
const ICON_FILE = /^[A-Za-z0-9-]+\.png$/;
|
|
37
|
+
const ICON_FILE = /^[A-Za-z0-9-]+\.(png|svg)$/;
|
|
38
|
+
|
|
39
|
+
/** The icon files the page, the manifest and the sign-in pages link. */
|
|
40
|
+
const PUBLIC_ICONS: ReadonlySet<string> = new Set([
|
|
41
|
+
"192.png",
|
|
42
|
+
"512.png",
|
|
43
|
+
"maskable-512.png",
|
|
44
|
+
"apple-180.png",
|
|
45
|
+
"badge-96.png",
|
|
46
|
+
"mark.svg",
|
|
47
|
+
"mark-maskable.svg",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/** The manifest and the linked icons answer without a session: a sign-in page and a link preview have none. Any other file of a host's icon directory stays behind it. */
|
|
51
|
+
export const isPublicStatic = (rest: string): boolean =>
|
|
52
|
+
rest === "manifest.webmanifest" ||
|
|
53
|
+
(rest.startsWith("icons/") && PUBLIC_ICONS.has(rest.slice("icons/".length)));
|
|
54
|
+
|
|
55
|
+
/** Whether the icon directory has the round-table mark as an SVG, which a tab shows crisper than a PNG. */
|
|
56
|
+
export const hasMarkSvg = (iconsDir: string | undefined): boolean =>
|
|
57
|
+
existsSync(join(iconsDir ?? DEFAULT_ICONS_DIR, "mark.svg"));
|
|
38
58
|
|
|
39
59
|
export interface PwaOptions {
|
|
40
60
|
mount: string;
|
|
@@ -116,10 +136,18 @@ export function servePwa(
|
|
|
116
136
|
const body = readFileSync(
|
|
117
137
|
join(options.iconsDir ?? DEFAULT_ICONS_DIR, name),
|
|
118
138
|
);
|
|
139
|
+
const svg = name.endsWith(".svg");
|
|
119
140
|
return new Response(new Uint8Array(body), {
|
|
120
141
|
headers: {
|
|
121
|
-
"content-type": "image/png",
|
|
122
|
-
"cache-control": "
|
|
142
|
+
"content-type": svg ? "image/svg+xml" : "image/png",
|
|
143
|
+
"cache-control": "public, max-age=86400",
|
|
144
|
+
// An SVG opened as a page must not run anything of its own.
|
|
145
|
+
...(svg
|
|
146
|
+
? {
|
|
147
|
+
"content-security-policy":
|
|
148
|
+
"default-src 'none'; style-src 'unsafe-inline'",
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
123
151
|
},
|
|
124
152
|
});
|
|
125
153
|
} catch {
|
package/src/web-app.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { ChannelKey, ServiceKey, Tier } from "pi-roundtable";
|
|
2
2
|
import { PluginError, serviceKey } from "pi-roundtable";
|
|
3
|
+
import type { ChannelContext } from "pi-roundtable/discord";
|
|
3
4
|
import type {
|
|
4
5
|
FormFieldView,
|
|
5
6
|
FormSpecView,
|
|
6
7
|
HostActionView,
|
|
7
8
|
HostAuthorKind,
|
|
9
|
+
HostIconName,
|
|
8
10
|
PictureRef,
|
|
9
11
|
RoomFile,
|
|
12
|
+
SendWording,
|
|
10
13
|
} from "./api-types.ts";
|
|
14
|
+
import { HOST_ICONS } from "./api-types.ts";
|
|
11
15
|
import type { Visitor } from "./visitors.ts";
|
|
12
16
|
|
|
13
|
-
export type { HostAuthorKind, PictureRef };
|
|
17
|
+
export type { HostAuthorKind, HostIconName, PictureRef, SendWording };
|
|
18
|
+
|
|
19
|
+
/** A host's icon name as the page may show it: one of `HOST_ICONS`, else none (the default shows). */
|
|
20
|
+
export const hostIcon = (name: unknown): HostIconName | undefined =>
|
|
21
|
+
HOST_ICONS.find((icon) => icon === name);
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
24
|
* Who a host plugin's callbacks serve: the person behind a request or a socket frame, with the
|
|
@@ -104,6 +112,15 @@ export interface RoomHeader {
|
|
|
104
112
|
export interface HostRoomKind {
|
|
105
113
|
kind: string;
|
|
106
114
|
label: string;
|
|
115
|
+
/** An icon of the console's own set, shown beside the kind's section; none = no icon. */
|
|
116
|
+
icon?: HostIconName;
|
|
117
|
+
/**
|
|
118
|
+
* The kind's own words for writing in its rooms. Where a viewer may send both a note and a turn,
|
|
119
|
+
* the room shows one toggle, "@AI" (off = a note for the room's people only, on =
|
|
120
|
+
* the agent answers): `turn.label` words the toggle, and each mode's `placeholder` is the box's
|
|
121
|
+
* hint while that mode is on. Without it the console's own words show.
|
|
122
|
+
*/
|
|
123
|
+
sendWording?: SendWording;
|
|
107
124
|
/** Rooms this viewer may open. Undefined = the kind is not shown to them. */
|
|
108
125
|
list(
|
|
109
126
|
viewer: Viewer,
|
|
@@ -129,7 +146,11 @@ export interface HostRoomKind {
|
|
|
129
146
|
input: FormValues,
|
|
130
147
|
): Promise<ActionResult>;
|
|
131
148
|
/** The buttons of a HOST card this viewer may press; default all when `access.act`. Never applies to held-call or question cards. */
|
|
132
|
-
buttons?(
|
|
149
|
+
buttons?(
|
|
150
|
+
viewer: Viewer,
|
|
151
|
+
id: string,
|
|
152
|
+
card: StoredHostCard,
|
|
153
|
+
): readonly string[] | Promise<readonly string[]>;
|
|
133
154
|
/** How long the kind's messages, author names, host cards and files are kept. Unset = keep. */
|
|
134
155
|
retention?: { textDays: number; fileDays: number };
|
|
135
156
|
}
|
|
@@ -143,6 +164,12 @@ export interface HostCard {
|
|
|
143
164
|
/** `https` or same-origin only. */
|
|
144
165
|
links?: readonly { label: string; url: string }[];
|
|
145
166
|
buttons?: readonly HostAction[];
|
|
167
|
+
/**
|
|
168
|
+
* Keeps the card past its kind's `retention.textDays`, for as long as the host passes `true`: a
|
|
169
|
+
* ticket's pinned card while the ticket is open. The sweep skips it; `updateCard` without `keep`
|
|
170
|
+
* (or with `false`) makes it an ordinary card again, counted from when it was first posted.
|
|
171
|
+
*/
|
|
172
|
+
keep?: boolean;
|
|
146
173
|
}
|
|
147
174
|
|
|
148
175
|
/** A host card as it is stored. */
|
|
@@ -197,6 +224,17 @@ export interface HostRooms {
|
|
|
197
224
|
/** Deletes the row and its files now. */
|
|
198
225
|
remove(room: ChannelKey, id: number): Promise<void>;
|
|
199
226
|
byReference(room: ChannelKey, reference: string): Promise<number | undefined>;
|
|
227
|
+
/**
|
|
228
|
+
* A room's recent messages as the block `CHANNEL_CONTEXT.read` gives a turn: what was said
|
|
229
|
+
* since the assistant last posted, at most `limit` (default 15, at most 100) newest messages,
|
|
230
|
+
* an outsider's words marked `external`, notices left out. For a host that starts a turn from
|
|
231
|
+
* a room's `send` hook and has no message that addressed the assistant. Unlike `read` it marks
|
|
232
|
+
* nothing handed over, so a retry gets the same; undefined when the room has nothing to give.
|
|
233
|
+
*/
|
|
234
|
+
context(
|
|
235
|
+
room: ChannelKey,
|
|
236
|
+
options?: { limit?: number; messageChars?: number },
|
|
237
|
+
): Promise<ChannelContext | undefined>;
|
|
200
238
|
card(room: ChannelKey, card: HostCard): Promise<{ id: string }>;
|
|
201
239
|
updateCard(room: ChannelKey, id: string, card: HostCard): Promise<void>;
|
|
202
240
|
saveFile(
|
|
@@ -237,6 +275,8 @@ export interface HostPage {
|
|
|
237
275
|
/** `[a-z0-9-]{1,40}`. */
|
|
238
276
|
id: string;
|
|
239
277
|
label: string;
|
|
278
|
+
/** An icon of the console's own set, shown in the navigation; none = a list. */
|
|
279
|
+
icon?: HostIconName;
|
|
240
280
|
place: "main" | "settings" | "hidden";
|
|
241
281
|
visible(viewer: Viewer): boolean | Promise<boolean>;
|
|
242
282
|
view(
|
|
@@ -255,7 +295,11 @@ export interface HostPage {
|
|
|
255
295
|
/** Person-addressed notices with state: the bell, the badge, push. */
|
|
256
296
|
export interface Notices {
|
|
257
297
|
raise(notice: {
|
|
258
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* The dedupe key: raising it again updates it and pushes again. It is also the push tag
|
|
300
|
+
* and the acknowledgement key, so the people the notice is for can see it: put no private
|
|
301
|
+
* id in it.
|
|
302
|
+
*/
|
|
259
303
|
tag: string;
|
|
260
304
|
/** Principal ids. */
|
|
261
305
|
to: readonly string[];
|