pi-roundtable-web 0.27.1 → 0.29.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 +53 -0
- package/README.md +25 -9
- package/dist/chunk-account-9616x405.js +1 -0
- package/dist/chunk-action-menu-q5y7xpry.js +1 -0
- package/dist/chunk-agent-x74xezyt.js +1 -0
- package/dist/chunk-arranged-rooms-9prq46vh.js +1 -0
- package/dist/chunk-bulk-actions-5dc4m9ad.js +1 -0
- package/dist/chunk-character-creator-wf02eg0h.js +1 -0
- package/dist/chunk-codeeditor-239ss6a2.js +2 -0
- package/dist/chunk-confirm-bhrx9195.js +1 -0
- package/dist/chunk-connectors-k77pd9by.js +37 -0
- package/dist/chunk-conversations-ftcpaqw1.js +1 -0
- package/dist/chunk-council-sheet-a5e6tspr.js +2 -0
- package/dist/chunk-council-view-6593rqbr.js +1 -0
- package/dist/chunk-drag-7873fvzw.js +1 -0
- package/dist/chunk-entry-5zmjxadx.js +1 -0
- package/dist/chunk-folder-list-h9hmbm8e.js +1 -0
- package/dist/chunk-folds-vxq86t1s.js +1 -0
- package/dist/chunk-gate-fa6za28z.js +1 -0
- package/dist/chunk-host-icons-y6frbzqt.js +1 -0
- package/dist/chunk-host-picture-fqg55rbr.js +1 -0
- package/dist/chunk-host-state-65tdhkyq.js +1 -0
- package/dist/chunk-host-ui-233kz7qp.js +1 -0
- package/dist/{chunk-index-j4x9dq30.css → chunk-index-1652v386.css} +1 -1
- package/dist/chunk-index-2c00zf3z.js +2 -0
- package/dist/chunk-index-2dhrqvba.js +2 -0
- package/dist/chunk-index-2s4bkrz3.js +2 -0
- package/dist/chunk-index-2ypaqwjy.js +2 -0
- package/dist/{chunk-index-k0z507h3.js → chunk-index-35zqk1zr.js} +2 -2
- package/dist/chunk-index-39qq37j5.js +2 -0
- package/dist/{chunk-index-j7pgphx3.js → chunk-index-3tjmyd1h.js} +2 -2
- package/dist/chunk-index-4eag6s50.js +2 -0
- package/dist/chunk-index-4jnn2g09.js +2 -0
- package/dist/{chunk-index-y60x5jbh.js → chunk-index-4krm48hm.js} +1 -1
- package/dist/{chunk-index-w42rcbx1.js → chunk-index-4re5prfp.js} +2 -2
- package/dist/chunk-index-57teqgct.js +2 -0
- package/dist/{chunk-index-3d8s3565.js → chunk-index-5ssncptg.js} +2 -2
- package/dist/{chunk-index-yrmqcay4.js → chunk-index-7c9fn21n.js} +2 -2
- package/dist/chunk-index-7k5895sn.js +14 -0
- package/dist/chunk-index-93f3e06v.js +2 -0
- package/dist/chunk-index-9m9v1p7s.js +2 -0
- package/dist/chunk-index-a8c0ap8g.js +2 -0
- package/dist/chunk-index-a8nqn3y0.js +3 -0
- package/dist/chunk-index-am7ghh75.js +120 -0
- package/dist/chunk-index-amspdq2j.js +4 -0
- package/dist/chunk-index-amxa2bn3.js +2 -0
- package/dist/{chunk-index-nsvwbjfe.js → chunk-index-aycxt14t.js} +2 -2
- package/dist/{chunk-index-n05yw5wd.js → chunk-index-bjeg3e2j.js} +2 -2
- package/dist/chunk-index-cttrqfm8.js +2 -0
- package/dist/chunk-index-dajanzxc.js +1 -0
- package/dist/chunk-index-e2eeckhh.js +2 -0
- package/dist/chunk-index-e46652wf.js +2 -0
- package/dist/chunk-index-ek98d2rn.js +2 -0
- package/dist/{chunk-index-f500nw4w.js → chunk-index-en5dm1zv.js} +2 -2
- package/dist/chunk-index-evhaaqzk.js +13 -0
- package/dist/chunk-index-f4gqzfkv.js +2 -0
- package/dist/{chunk-index-ak6ecqy3.js → chunk-index-gj3n4dh5.js} +2 -2
- package/dist/chunk-index-gp4chnp3.js +2 -0
- package/dist/{chunk-index-af4ebmkr.js → chunk-index-h2a42p6s.js} +3 -3
- package/dist/{chunk-index-4z4chg7v.js → chunk-index-haac6nbt.js} +2 -2
- package/dist/{chunk-index-3pnvpppa.js → chunk-index-hef4gwtb.js} +2 -2
- package/dist/chunk-index-mg96sqpk.js +2 -0
- package/dist/chunk-index-nsya88fn.js +1 -0
- package/dist/{chunk-index-ben4gv45.js → chunk-index-ntt3em2k.js} +1 -1
- package/dist/chunk-index-pcbkp62w.js +2 -0
- package/dist/chunk-index-pfs6rm6w.js +2 -0
- package/dist/chunk-index-pgxgq61x.js +2 -0
- package/dist/{chunk-index-9z13jw6q.js → chunk-index-q33ppq0j.js} +3 -3
- package/dist/chunk-index-r0hk9kyd.js +47 -0
- package/dist/{chunk-index-0ye10vk3.js → chunk-index-rr73h4kc.js} +3 -3
- package/dist/{chunk-index-983dmeaa.js → chunk-index-rtz4936v.js} +3 -3
- package/dist/chunk-index-smz90j6p.js +2 -0
- package/dist/chunk-index-teyx2hz7.js +2 -0
- package/dist/{chunk-index-sm7m4fej.js → chunk-index-v6h43553.js} +2 -2
- package/dist/chunk-index-vzqwn3wp.js +2 -0
- package/dist/chunk-index-wt2asefx.js +50 -0
- package/dist/chunk-index-x4tq9btg.js +2 -0
- package/dist/chunk-index-y6p8j3zv.js +2 -0
- package/dist/chunk-index-yvcdg5hh.js +2 -0
- package/dist/{chunk-index-afrhnrxj.js → chunk-index-zdzzf3fp.js} +2 -2
- package/dist/chunk-index-zeqfnkty.js +2 -0
- package/dist/chunk-index-zpwb8mkk.js +2 -0
- package/dist/chunk-lang-css-avzhvm72.js +1 -0
- package/dist/{chunk-lang-dockerfile-cnwqn660.js → chunk-lang-dockerfile-2p7e3ssz.js} +1 -1
- package/dist/{chunk-lang-go-yy6kgsxv.js → chunk-lang-go-zxp9k517.js} +3 -3
- package/dist/chunk-lang-html-red02gep.js +1 -0
- package/dist/chunk-lang-javascript-n33kmjcx.js +1 -0
- package/dist/chunk-lang-json-zmym50rg.js +1 -0
- package/dist/chunk-lang-markdown-ww2kcs85.js +6 -0
- package/dist/chunk-lang-python-ng1zdtd4.js +3 -0
- package/dist/chunk-lang-rust-6ynhxjdd.js +1 -0
- package/dist/{chunk-lang-shell-1rpgtepw.js → chunk-lang-shell-39t9bz2s.js} +1 -1
- package/dist/chunk-lang-sql-w875ejpg.js +2 -0
- package/dist/{chunk-lang-toml-tbj7467c.js → chunk-lang-toml-b8ckgkg5.js} +1 -1
- package/dist/chunk-lang-typescript-qf8acbb2.js +1 -0
- package/dist/chunk-lang-xml-bz91cjp1.js +1 -0
- package/dist/chunk-lang-yaml-g9zy0tdv.js +1 -0
- package/dist/chunk-layout-actions-fyzzzeyp.js +1 -0
- package/dist/chunk-menu-items-yvnk3vds.js +1 -0
- package/dist/chunk-mode-toggle-ccv271hm.js +1 -0
- package/dist/chunk-note-row-314py1pp.js +204 -0
- package/dist/chunk-notes-4rnrddzw.js +1 -0
- package/dist/chunk-notice-bell-z9wgamnz.js +1 -0
- package/dist/chunk-notices-mtn7pg1z.js +1 -0
- package/dist/chunk-onboarding-f3hhxgg4.js +1 -0
- package/dist/chunk-overview-qmepntrr.js +1 -0
- package/dist/chunk-palette-edkn1ypq.js +1 -0
- package/dist/chunk-placement-items-trsa5t23.js +1 -0
- package/dist/chunk-profile-pwfq0wns.js +1 -0
- package/dist/chunk-prompt-history-xp9hmeqt.js +1 -0
- package/dist/chunk-prompts-a56qvytg.js +1 -0
- package/dist/chunk-room-confirms-gqpkz4h0.js +1 -0
- package/dist/chunk-room-menu-1m2c2ekt.js +1 -0
- package/dist/chunk-rooms-menu-button-tcvda1gq.js +1 -0
- package/dist/chunk-schedules-kwnw7p8v.js +1 -0
- package/dist/{chunk-seat-menu-kwtjnpjj.js → chunk-seat-menu-m2n9nvv0.js} +1 -1
- package/dist/chunk-sheet-hosts-pr8xktyq.js +29 -0
- package/dist/{chunk-shortcut-help-g5cpqjjj.js → chunk-shortcut-help-sbvrds1m.js} +2 -2
- package/dist/chunk-shortcuts-8705p7q3.js +1 -0
- package/dist/chunk-skill-editor-7xy09sq1.js +8 -0
- package/dist/chunk-skills-cnc25x7q.js +1 -0
- package/dist/chunk-start-over-line-97zdd9pb.js +16 -0
- package/dist/chunk-switcher-list-ehsn2wsk.js +1 -0
- package/dist/{chunk-tab-dot-yse6wcfp.js → chunk-tab-dot-gkpe06q8.js} +1 -1
- package/dist/chunk-table-av3kh3tt.js +1 -0
- package/dist/chunk-table2d-5x3jptp9.js +1 -0
- package/dist/chunk-table3d-rsvajx7r.js +4220 -0
- package/dist/chunk-thread-actions-7xtkegwm.js +1 -0
- package/dist/{chunk-thread-cards-36rckyg7.js → chunk-thread-cards-grzszfha.js} +1 -1
- package/dist/chunk-thread-list-9bjjac3j.js +217 -0
- package/dist/chunk-transcript-t5szb8r0.js +1 -0
- package/dist/chunk-waiting-chip-d8b6a0p8.js +28 -0
- package/dist/chunk-waiting-ts3kxjvv.js +1 -0
- package/dist/chunk-where-3d3ncsa5.js +1 -0
- package/dist/chunk-workspace-ct6hvn0z.js +578 -0
- package/dist/chunk-zh-tw-v9v1eerm.js +1 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +3 -3
- package/src/api-types.ts +137 -1
- package/src/app/app.ts +4 -7
- package/src/app/desk.ts +19 -4
- package/src/app/room-log.ts +66 -18
- package/src/app/rooms.ts +45 -7
- package/src/app/routes.ts +86 -11
- package/src/app/skill-doc-routes.ts +12 -2
- package/src/app/skill-group-routes.ts +126 -0
- package/src/app/surface.ts +26 -2
- package/src/app/team-routes.ts +3 -7
- package/src/authz.ts +2 -0
- package/src/cleanup-api.ts +9 -2
- package/src/connector-manage-api.ts +257 -0
- package/src/console-api.ts +60 -2
- package/src/features.ts +91 -0
- package/src/host/reads.ts +10 -1
- package/src/i18n/zh-tw.ts +137 -5
- package/src/push/channel.ts +1 -0
- package/src/web-plugin.ts +8 -1
- package/dist/chunk-account-83crfbn4.js +0 -1
- package/dist/chunk-action-menu-qw7h03x0.js +0 -1
- package/dist/chunk-agent-t1632ktv.js +0 -1
- package/dist/chunk-arranged-rooms-sb6tbsxw.js +0 -1
- package/dist/chunk-bulk-actions-v1g2bzk7.js +0 -1
- package/dist/chunk-character-creator-ye69w0kx.js +0 -1
- package/dist/chunk-codeeditor-5j6abj8k.js +0 -2
- package/dist/chunk-confirm-x8w877bz.js +0 -1
- package/dist/chunk-connectors-t7qbc6nk.js +0 -1
- package/dist/chunk-conversations-kmt6qzym.js +0 -1
- package/dist/chunk-council-sheet-bxvn7vza.js +0 -2
- package/dist/chunk-drag-03zgvd66.js +0 -1
- package/dist/chunk-entry-y5vwp5e5.js +0 -1
- package/dist/chunk-folder-list-h3vnjzpb.js +0 -1
- package/dist/chunk-folds-a3e22m7x.js +0 -1
- package/dist/chunk-gate-qzephvdv.js +0 -1
- package/dist/chunk-group-scene-w1ycabrm.js +0 -1
- package/dist/chunk-group-table-9nvmja8w.js +0 -1
- package/dist/chunk-host-icons-fccg3b2d.js +0 -1
- package/dist/chunk-host-picture-jaqz127q.js +0 -1
- package/dist/chunk-host-state-r01v1ywg.js +0 -1
- package/dist/chunk-host-ui-n54mh5af.js +0 -1
- package/dist/chunk-index-0a81da8z.js +0 -2
- package/dist/chunk-index-1kwp3spy.js +0 -2
- package/dist/chunk-index-47vq85ye.js +0 -2
- package/dist/chunk-index-5zmswz4s.js +0 -2
- package/dist/chunk-index-64rsz6pf.js +0 -2
- package/dist/chunk-index-6vcs0h0m.js +0 -2
- package/dist/chunk-index-6xqz6mg2.js +0 -2
- package/dist/chunk-index-83v0c9x6.js +0 -2
- package/dist/chunk-index-8xd6anam.js +0 -2
- package/dist/chunk-index-b73y2gc0.js +0 -47
- package/dist/chunk-index-bhqczxr3.js +0 -2
- package/dist/chunk-index-c44p8q87.js +0 -2
- package/dist/chunk-index-cbqhzf40.js +0 -13
- package/dist/chunk-index-db8zh8e6.js +0 -2
- package/dist/chunk-index-dvfkb699.js +0 -2
- package/dist/chunk-index-e02ddp46.js +0 -1
- package/dist/chunk-index-ebg48jgy.js +0 -2
- package/dist/chunk-index-ek5mek4j.js +0 -14
- package/dist/chunk-index-em7vndrb.js +0 -2
- package/dist/chunk-index-eq161yvp.js +0 -4
- package/dist/chunk-index-f5x2srr0.js +0 -2
- package/dist/chunk-index-fg42egv9.js +0 -50
- package/dist/chunk-index-h3v1d3nt.js +0 -2
- package/dist/chunk-index-kbgykazq.js +0 -2
- package/dist/chunk-index-n8ghdvxb.js +0 -2
- package/dist/chunk-index-p5jcwjx8.js +0 -2
- package/dist/chunk-index-qfgkfq86.js +0 -2
- package/dist/chunk-index-rcwdqn50.js +0 -7
- package/dist/chunk-index-s72hmse6.js +0 -2
- package/dist/chunk-index-sv9qpmhv.js +0 -1
- package/dist/chunk-index-x4enmj72.js +0 -2
- package/dist/chunk-index-xevnf9ew.js +0 -2
- package/dist/chunk-index-xx8jq73q.js +0 -2
- package/dist/chunk-index-ypkgw7gq.js +0 -2
- package/dist/chunk-lang-css-y9133ktr.js +0 -1
- package/dist/chunk-lang-html-r2xmryek.js +0 -1
- package/dist/chunk-lang-javascript-1r6wd177.js +0 -1
- package/dist/chunk-lang-json-dx56g11y.js +0 -1
- package/dist/chunk-lang-markdown-fpcf2dfq.js +0 -6
- package/dist/chunk-lang-python-zzp95955.js +0 -3
- package/dist/chunk-lang-rust-214fh7sp.js +0 -1
- package/dist/chunk-lang-sql-vvp46hpj.js +0 -2
- package/dist/chunk-lang-typescript-y5w4tt4g.js +0 -1
- package/dist/chunk-lang-xml-tbcqmedk.js +0 -1
- package/dist/chunk-lang-yaml-jh56t3sv.js +0 -1
- package/dist/chunk-layout-actions-8dzvysym.js +0 -1
- package/dist/chunk-menu-items-3jhya205.js +0 -2
- package/dist/chunk-mode-toggle-js49gvpd.js +0 -1
- package/dist/chunk-note-row-kv061030.js +0 -75
- package/dist/chunk-notes-sdkqz5hr.js +0 -1
- package/dist/chunk-notice-bell-r012f2zy.js +0 -1
- package/dist/chunk-notices-h4rk1zxc.js +0 -1
- package/dist/chunk-onboarding-a18cvkph.js +0 -1
- package/dist/chunk-overview-ynaqn4fp.js +0 -1
- package/dist/chunk-palette-jm79gb5q.js +0 -1
- package/dist/chunk-placement-items-mwm9q1zn.js +0 -1
- package/dist/chunk-profile-ajw6pp4r.js +0 -1
- package/dist/chunk-prompt-history-zcfvr3r8.js +0 -1
- package/dist/chunk-prompts-m5rzdk24.js +0 -1
- package/dist/chunk-room-confirms-ftsj15ja.js +0 -1
- package/dist/chunk-room-menu-b9ktw9pw.js +0 -1
- package/dist/chunk-rooms-menu-button-qe62t6nx.js +0 -1
- package/dist/chunk-schedules-t8yxnf8q.js +0 -1
- package/dist/chunk-sheet-hosts-yj5wszgj.js +0 -1
- package/dist/chunk-shortcuts-9ft5sqdw.js +0 -1
- package/dist/chunk-skill-editor-xdfq2708.js +0 -8
- package/dist/chunk-skills-9wa4mwdc.js +0 -1
- package/dist/chunk-switcher-list-vpg6wvtw.js +0 -1
- package/dist/chunk-table-1y76m54h.js +0 -1
- package/dist/chunk-table2d-g57vr6h0.js +0 -1
- package/dist/chunk-table3d-cv6069hy.js +0 -4220
- package/dist/chunk-thread-actions-xjhpdjk7.js +0 -1
- package/dist/chunk-thread-list-fd6agz77.js +0 -217
- package/dist/chunk-transcript-5nfn3r35.js +0 -1
- package/dist/chunk-where-ha8pc3cg.js +0 -1
- package/dist/chunk-workspace-e85twv32.js +0 -578
- package/dist/chunk-zh-tw-z5p5mpph.js +0 -1
package/src/app/room-log.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { SQL } from "bun";
|
|
2
2
|
import type { Migration } from "pi-roundtable";
|
|
3
|
-
import
|
|
4
|
-
HostAuthorKind,
|
|
5
|
-
PictureRef,
|
|
6
|
-
RoomFile,
|
|
7
|
-
|
|
3
|
+
import {
|
|
4
|
+
type HostAuthorKind,
|
|
5
|
+
type PictureRef,
|
|
6
|
+
type RoomFile,
|
|
7
|
+
type RoomIntercom,
|
|
8
|
+
type RoomMessage,
|
|
9
|
+
START_OVER_TEXT,
|
|
8
10
|
} from "../api-types.ts";
|
|
9
11
|
import { storedJson } from "../host/stored-json.ts";
|
|
10
12
|
|
|
@@ -27,6 +29,8 @@ export interface NewRoomMessage {
|
|
|
27
29
|
at?: Date;
|
|
28
30
|
/** An internal note: shown to the room's readers, never handed to the agent. */
|
|
29
31
|
note?: true;
|
|
32
|
+
/** A message between agents, drawn as a small card. */
|
|
33
|
+
intercom?: RoomIntercom;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
interface Row {
|
|
@@ -44,6 +48,7 @@ interface Row {
|
|
|
44
48
|
picture: PictureRef | string | null;
|
|
45
49
|
edited_at: Date | null;
|
|
46
50
|
note: boolean | null;
|
|
51
|
+
intercom?: RoomIntercom | string | null;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** The most messages one page of history holds. */
|
|
@@ -70,6 +75,9 @@ function messageOf(row: Row): RoomMessage {
|
|
|
70
75
|
}),
|
|
71
76
|
...(row.edited_at === null ? {} : { edited: true as const }),
|
|
72
77
|
...(row.note ? { note: true as const } : {}),
|
|
78
|
+
...(row.intercom == null
|
|
79
|
+
? {}
|
|
80
|
+
: { intercom: storedJson(row.intercom, "intercom card") }),
|
|
73
81
|
};
|
|
74
82
|
}
|
|
75
83
|
|
|
@@ -128,6 +136,22 @@ export class RoomLog {
|
|
|
128
136
|
},
|
|
129
137
|
};
|
|
130
138
|
|
|
139
|
+
/** The column that marks a message between agents, with which way it went and the other agent. */
|
|
140
|
+
static readonly intercomMigration: Migration = {
|
|
141
|
+
name: "web-room-messages-intercom",
|
|
142
|
+
up: async (sql) => {
|
|
143
|
+
await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS intercom jsonb`;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** Finds a room's start-over lines without reading the whole room. */
|
|
148
|
+
static readonly startOverMigration: Migration = {
|
|
149
|
+
name: "web-room-messages-start-over",
|
|
150
|
+
up: async (sql) => {
|
|
151
|
+
await sql`CREATE INDEX IF NOT EXISTS web_room_messages_start_over ON web_room_messages (room, id) WHERE kind = 'system' AND text = 'Conversation started over'`;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
131
155
|
async append(message: NewRoomMessage): Promise<RoomMessage> {
|
|
132
156
|
const [row] = await this.#insert(message, false);
|
|
133
157
|
if (!row) throw new Error("the room log stored nothing");
|
|
@@ -153,24 +177,25 @@ export class RoomLog {
|
|
|
153
177
|
async #insert(message: NewRoomMessage, once: boolean): Promise<Row[]> {
|
|
154
178
|
const files = JSON.stringify(message.files ?? []);
|
|
155
179
|
const picture = message.picture ? JSON.stringify(message.picture) : null;
|
|
180
|
+
const intercom = message.intercom ? JSON.stringify(message.intercom) : null;
|
|
156
181
|
const at = message.at ?? new Date();
|
|
157
182
|
return once
|
|
158
183
|
? await this.#sql`
|
|
159
184
|
INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
|
|
160
|
-
author_kind, author_label, picture, reference, created_at, note)
|
|
185
|
+
author_kind, author_label, picture, reference, created_at, note, intercom)
|
|
161
186
|
VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
|
|
162
187
|
${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
|
|
163
188
|
${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
|
|
164
|
-
${message.reference ?? null}, ${at}, ${message.note === true})
|
|
189
|
+
${message.reference ?? null}, ${at}, ${message.note === true}, ${intercom}::jsonb)
|
|
165
190
|
ON CONFLICT (room, reference) WHERE reference IS NOT NULL DO NOTHING
|
|
166
191
|
RETURNING *`
|
|
167
192
|
: await this.#sql`
|
|
168
193
|
INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
|
|
169
|
-
author_kind, author_label, picture, reference, created_at, note)
|
|
194
|
+
author_kind, author_label, picture, reference, created_at, note, intercom)
|
|
170
195
|
VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
|
|
171
196
|
${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
|
|
172
197
|
${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
|
|
173
|
-
${message.reference ?? null}, ${at}, ${message.note === true})
|
|
198
|
+
${message.reference ?? null}, ${at}, ${message.note === true}, ${intercom}::jsonb)
|
|
174
199
|
RETURNING *`;
|
|
175
200
|
}
|
|
176
201
|
|
|
@@ -241,27 +266,50 @@ export class RoomLog {
|
|
|
241
266
|
return found;
|
|
242
267
|
}
|
|
243
268
|
|
|
244
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* The newest `limit` messages older than `before` (all of them when absent), oldest first, of
|
|
271
|
+
* the room's conversation: from its latest start-over line, which the page includes, on. With
|
|
272
|
+
* `until`, the id of one of its start-over lines, those of the conversation that line ended
|
|
273
|
+
* instead: older than the line, back to the start-over line before it.
|
|
274
|
+
*/
|
|
245
275
|
async page(
|
|
246
276
|
room: string,
|
|
247
277
|
before: number | undefined,
|
|
248
278
|
limit: number,
|
|
279
|
+
until?: number,
|
|
249
280
|
): Promise<{ messages: RoomMessage[]; more: boolean }> {
|
|
250
281
|
const count = Math.min(Math.max(Math.trunc(limit), 1), PAGE_LIMIT);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
282
|
+
const start = await this.startOver(room, until);
|
|
283
|
+
// The live conversation includes its own start line; an archived one ends before the next.
|
|
284
|
+
const above = until === undefined ? (start ?? 1) - 1 : (start ?? 0);
|
|
285
|
+
const below =
|
|
286
|
+
until === undefined
|
|
287
|
+
? before
|
|
288
|
+
: before === undefined
|
|
289
|
+
? until
|
|
290
|
+
: Math.min(before, until);
|
|
291
|
+
const rows: Row[] = await this.#sql`
|
|
292
|
+
SELECT * FROM web_room_messages
|
|
293
|
+
WHERE room = ${room} AND id > ${above}
|
|
294
|
+
AND (${below ?? null}::bigint IS NULL OR id < ${below ?? null}::bigint)
|
|
295
|
+
ORDER BY id DESC LIMIT ${count + 1}`;
|
|
259
296
|
return {
|
|
260
297
|
messages: rows.slice(0, count).map(messageOf).reverse(),
|
|
261
298
|
more: rows.length > count,
|
|
262
299
|
};
|
|
263
300
|
}
|
|
264
301
|
|
|
302
|
+
/** The id of the room's latest start-over line, or of the latest one older than `before`. */
|
|
303
|
+
async startOver(room: string, before?: number): Promise<number | undefined> {
|
|
304
|
+
const [row]: { id: string | number | bigint | null }[] = await this.#sql`
|
|
305
|
+
SELECT max(id) AS id FROM web_room_messages
|
|
306
|
+
WHERE room = ${room} AND kind = 'system' AND text = ${START_OVER_TEXT}
|
|
307
|
+
AND (${before ?? null}::bigint IS NULL OR id < ${before ?? null}::bigint)`;
|
|
308
|
+
return row?.id === null || row?.id === undefined
|
|
309
|
+
? undefined
|
|
310
|
+
: Number(row.id);
|
|
311
|
+
}
|
|
312
|
+
|
|
265
313
|
/** Deletes every message of a room that is gone for good; returns how many. */
|
|
266
314
|
async removeRoom(room: string): Promise<number> {
|
|
267
315
|
const rows: unknown[] = await this
|
package/src/app/rooms.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
AgentTeam,
|
|
6
6
|
AvatarStudio,
|
|
7
7
|
ChannelKey,
|
|
8
|
+
ConversationMode,
|
|
8
9
|
ConversationModes,
|
|
9
10
|
} from "pi-roundtable";
|
|
10
11
|
import {
|
|
@@ -12,10 +13,22 @@ import {
|
|
|
12
13
|
channelKey,
|
|
13
14
|
characterOf,
|
|
14
15
|
groupRoomKey,
|
|
16
|
+
helperSurface,
|
|
15
17
|
isHelper,
|
|
16
18
|
roomOf,
|
|
17
19
|
} from "pi-roundtable";
|
|
18
|
-
import type { RoomKind, RoomView } from "../api-types.ts";
|
|
20
|
+
import type { RoomKind, RoomMode, RoomView } from "../api-types.ts";
|
|
21
|
+
|
|
22
|
+
/** A conversation's modes as a room shows them: an inherited one names where it comes from. */
|
|
23
|
+
export function roomModeOf(mode: ConversationMode): RoomMode {
|
|
24
|
+
const from = mode.inheritedFrom;
|
|
25
|
+
return {
|
|
26
|
+
yolo: mode.yolo,
|
|
27
|
+
longRunning: mode.longRunning,
|
|
28
|
+
...(from ? { inheritedFrom: from.name ?? from.channel } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
import type { PushMutes } from "../push/mutes.ts";
|
|
20
33
|
import type { RoomLog } from "./room-log.ts";
|
|
21
34
|
import type { WebThreads } from "./threads.ts";
|
|
@@ -62,6 +75,8 @@ const THREAD_ID = /^thread\.[0-9a-f-]{36}$/;
|
|
|
62
75
|
*/
|
|
63
76
|
export class Rooms {
|
|
64
77
|
readonly #options: RoomsOptions;
|
|
78
|
+
/** The rooms of temp knights made on Discord that someone opened by their address here. */
|
|
79
|
+
readonly #opened = new Set<string>();
|
|
65
80
|
|
|
66
81
|
constructor(options: RoomsOptions) {
|
|
67
82
|
this.#options = options;
|
|
@@ -176,12 +191,11 @@ export class Rooms {
|
|
|
176
191
|
const thread = await this.#options.threads.get(room.name);
|
|
177
192
|
if (!thread) return undefined;
|
|
178
193
|
const { modes, activity, log } = this.#options;
|
|
179
|
-
const { yolo, longRunning } = modes.peek(key);
|
|
180
194
|
const last = (await log.lastOf([key])).get(key);
|
|
181
195
|
return {
|
|
182
196
|
home: false,
|
|
183
197
|
readOnly: true,
|
|
184
|
-
mode:
|
|
198
|
+
mode: roomModeOf(modes.peek(key)),
|
|
185
199
|
...activity(key),
|
|
186
200
|
kind: "thread",
|
|
187
201
|
name: thread.id,
|
|
@@ -193,6 +207,19 @@ export class Rooms {
|
|
|
193
207
|
};
|
|
194
208
|
}
|
|
195
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The view of a temp knight's room that the list leaves out, opened by its address: from then on
|
|
212
|
+
* the list shows it. Undefined for any other room.
|
|
213
|
+
*/
|
|
214
|
+
async openHelper(key: ChannelKey): Promise<RoomView | undefined> {
|
|
215
|
+
const room = await this.resolve(key);
|
|
216
|
+
if (room?.kind !== "agent") return undefined;
|
|
217
|
+
const agent = this.#options.directory.agent(room.name);
|
|
218
|
+
if (helperSurface(agent) !== "discord") return undefined;
|
|
219
|
+
this.#opened.add(room.name);
|
|
220
|
+
return (await this.list()).find((one) => one.key === key);
|
|
221
|
+
}
|
|
222
|
+
|
|
196
223
|
async list(): Promise<RoomView[]> {
|
|
197
224
|
const { surface, directory, modes, mutes, activity, log, threads } =
|
|
198
225
|
this.#options;
|
|
@@ -207,11 +234,10 @@ export class Rooms {
|
|
|
207
234
|
key: ChannelKey,
|
|
208
235
|
rest: Pick<RoomView, "kind" | "name" | "displayName"> & Partial<RoomView>,
|
|
209
236
|
): RoomView => {
|
|
210
|
-
const { yolo, longRunning } = modes.peek(key);
|
|
211
237
|
return {
|
|
212
238
|
home: key === home,
|
|
213
239
|
readOnly: false,
|
|
214
|
-
mode:
|
|
240
|
+
mode: roomModeOf(modes.peek(key)),
|
|
215
241
|
...(muted.has(key) ? { muted: true as const } : {}),
|
|
216
242
|
...(notes && rest.kind !== "thread" ? { notes: true as const } : {}),
|
|
217
243
|
...activity(key),
|
|
@@ -264,9 +290,21 @@ export class Rooms {
|
|
|
264
290
|
// Home first; the others keep the team's order.
|
|
265
291
|
rooms.sort((a, b) => Number(b.home) - Number(a.home));
|
|
266
292
|
const last = await log.lastOf(rooms.map((room) => room.key));
|
|
267
|
-
|
|
293
|
+
// A temp knight made on Discord works there: its room here shows once it has a message or
|
|
294
|
+
// someone opened it by its address.
|
|
295
|
+
const quiet = new Set<string>(
|
|
296
|
+
agents
|
|
297
|
+
.filter(
|
|
298
|
+
(agent) =>
|
|
299
|
+
helperSurface(agent) === "discord" && !this.#opened.has(agent.name),
|
|
300
|
+
)
|
|
301
|
+
.map((agent) => agentRoomKey(surface, agent.name))
|
|
302
|
+
.filter((key) => !last.has(key)),
|
|
303
|
+
);
|
|
304
|
+
return rooms.flatMap((room) => {
|
|
305
|
+
if (quiet.has(room.key)) return [];
|
|
268
306
|
const message = last.get(room.key);
|
|
269
|
-
return message ? { ...room, last: message } : room;
|
|
307
|
+
return [message ? { ...room, last: message } : room];
|
|
270
308
|
});
|
|
271
309
|
}
|
|
272
310
|
}
|
package/src/app/routes.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AgentDirectory,
|
|
3
3
|
AgentTeam,
|
|
4
4
|
AvatarStudio,
|
|
5
|
+
ChannelKey,
|
|
5
6
|
ConversationModes,
|
|
6
7
|
ConversationPort,
|
|
7
8
|
IdentityService,
|
|
@@ -37,6 +38,7 @@ import type {
|
|
|
37
38
|
WorkspaceRootsView,
|
|
38
39
|
WorkspaceSave,
|
|
39
40
|
} from "../api-types.ts";
|
|
41
|
+
import { isStartOver } from "../api-types.ts";
|
|
40
42
|
import type { Authenticated } from "../auth/authenticator.ts";
|
|
41
43
|
import { type ConsolePresentation, message } from "../features.ts";
|
|
42
44
|
import type { Gate } from "../gate.ts";
|
|
@@ -56,8 +58,9 @@ import type { LayoutStore } from "./layout.ts";
|
|
|
56
58
|
import { LayoutRoutes } from "./layout-routes.ts";
|
|
57
59
|
import { PromptApi, type PromptApiOptions } from "./prompt-routes.ts";
|
|
58
60
|
import { PAGE_LIMIT, type RoomLog } from "./room-log.ts";
|
|
59
|
-
import type
|
|
61
|
+
import { type ResolvedRoom, type Rooms, roomModeOf } from "./rooms.ts";
|
|
60
62
|
import { SkillDocApi } from "./skill-doc-routes.ts";
|
|
63
|
+
import { SkillGroupApi } from "./skill-group-routes.ts";
|
|
61
64
|
import type { AppSurface } from "./surface.ts";
|
|
62
65
|
import { TeamApi } from "./team-routes.ts";
|
|
63
66
|
import type { WebThreads } from "./threads.ts";
|
|
@@ -79,12 +82,12 @@ export interface AppApiOptions {
|
|
|
79
82
|
presentation?: ConsolePresentation;
|
|
80
83
|
surface: AppSurface;
|
|
81
84
|
rooms: Rooms;
|
|
82
|
-
log: Pick<RoomLog, "page" | "lastOf" | "removeRoom">;
|
|
85
|
+
log: Pick<RoomLog, "page" | "lastOf" | "removeRoom" | "get" | "startOver">;
|
|
83
86
|
cards: Pick<CardLog, "during" | "removeRoom">;
|
|
84
87
|
uploads: Uploads;
|
|
85
88
|
/** Where stored files are read from. */
|
|
86
89
|
files: FileDir;
|
|
87
|
-
modes: Pick<ConversationModes, "set" | "peek">;
|
|
90
|
+
modes: Pick<ConversationModes, "set" | "peek" | "forget">;
|
|
88
91
|
/** The rooms whose push the owners muted. */
|
|
89
92
|
mutes: Pick<PushMutes, "set" | "setMany">;
|
|
90
93
|
/** The dispatch threads, which go with the room they hang under. */
|
|
@@ -131,11 +134,17 @@ export interface AppApiOptions {
|
|
|
131
134
|
add: readonly string[],
|
|
132
135
|
remove: readonly string[],
|
|
133
136
|
): Promise<string[]>;
|
|
134
|
-
/** The registry's documents: a skill's SKILL.md read, saved and
|
|
137
|
+
/** The registry's documents: a skill's SKILL.md read, saved, made and deleted, and its versions. */
|
|
135
138
|
documents: Pick<
|
|
136
139
|
SkillRegistry,
|
|
137
|
-
|
|
140
|
+
| "document"
|
|
141
|
+
| "saveDocument"
|
|
142
|
+
| "createDocument"
|
|
143
|
+
| "documentHistory"
|
|
144
|
+
| "delete"
|
|
138
145
|
>;
|
|
146
|
+
/** The registry's skill groups: listed, saved and deleted. */
|
|
147
|
+
groups: Pick<SkillRegistry, "groups" | "setGroup" | "deleteGroup">;
|
|
139
148
|
};
|
|
140
149
|
/** Present when the host has prompt documents (the prompts plugin): the main prompts the owner edits. */
|
|
141
150
|
prompts?: PromptApiOptions["prompts"];
|
|
@@ -279,6 +288,7 @@ export class AppApi {
|
|
|
279
288
|
readonly #helpers: HelperRoutes;
|
|
280
289
|
readonly #prompts: PromptApi | undefined;
|
|
281
290
|
readonly #skillDocs: SkillDocApi | undefined;
|
|
291
|
+
readonly #skillGroups: SkillGroupApi | undefined;
|
|
282
292
|
|
|
283
293
|
constructor(options: AppApiOptions) {
|
|
284
294
|
this.#options = options;
|
|
@@ -305,6 +315,17 @@ export class AppApi {
|
|
|
305
315
|
catalog: () => options.skills?.catalog() ?? [],
|
|
306
316
|
})
|
|
307
317
|
: undefined;
|
|
318
|
+
const skills = options.skills;
|
|
319
|
+
this.#skillGroups = skills
|
|
320
|
+
? new SkillGroupApi({
|
|
321
|
+
groups: skills.groups,
|
|
322
|
+
catalog: () => skills.catalog(),
|
|
323
|
+
attach: (agent, add, remove) => skills.attach(agent, add, remove),
|
|
324
|
+
active: (agent) =>
|
|
325
|
+
options.directory.agent(agent)?.status === "active",
|
|
326
|
+
changed: () => options.changed(),
|
|
327
|
+
})
|
|
328
|
+
: undefined;
|
|
308
329
|
this.#team = new TeamApi({
|
|
309
330
|
surface: options.surface.surface,
|
|
310
331
|
rooms: options.rooms,
|
|
@@ -388,6 +409,7 @@ export class AppApi {
|
|
|
388
409
|
"workspace",
|
|
389
410
|
"prompts",
|
|
390
411
|
"skill-docs",
|
|
412
|
+
"skill-groups",
|
|
391
413
|
].includes(second ?? ""));
|
|
392
414
|
if (!mine) return undefined;
|
|
393
415
|
try {
|
|
@@ -486,6 +508,11 @@ export class AppApi {
|
|
|
486
508
|
throw new HttpError(404, "This host has no skills.");
|
|
487
509
|
return this.#skillDocs.handle(request, parts, owner);
|
|
488
510
|
}
|
|
511
|
+
if (area === "skill-groups") {
|
|
512
|
+
if (!this.#skillGroups)
|
|
513
|
+
throw new HttpError(404, "This host has no skills.");
|
|
514
|
+
return this.#skillGroups.handle(request, parts, owner);
|
|
515
|
+
}
|
|
489
516
|
throw new HttpError(404, "There is no such API.");
|
|
490
517
|
}
|
|
491
518
|
|
|
@@ -559,8 +586,11 @@ export class AppApi {
|
|
|
559
586
|
const room = await rooms.resolve(key);
|
|
560
587
|
if (!room) throw notFound();
|
|
561
588
|
if (!action && method === "GET") {
|
|
562
|
-
// A thread dismissed from the list
|
|
563
|
-
|
|
589
|
+
// A thread dismissed from the list, or a temp knight's room the list leaves out, still opens
|
|
590
|
+
// by its address; the page asks for its view.
|
|
591
|
+
const view =
|
|
592
|
+
(await rooms.threadView(room.key)) ??
|
|
593
|
+
(await rooms.openHelper(room.key));
|
|
564
594
|
if (!view) throw notFound();
|
|
565
595
|
return json({ room: view } satisfies RoomOneView);
|
|
566
596
|
}
|
|
@@ -580,17 +610,23 @@ export class AppApi {
|
|
|
580
610
|
const params = new URL(request.url).searchParams;
|
|
581
611
|
const before = params.get("before");
|
|
582
612
|
const limit = params.get("limit");
|
|
613
|
+
const archive = params.get("archive");
|
|
583
614
|
const beforeId = before === null ? undefined : Number(before);
|
|
615
|
+
const archiveId = archive === null ? undefined : Number(archive);
|
|
584
616
|
const count = limit === null ? PAGE_LIMIT : Number(limit);
|
|
585
617
|
if (
|
|
586
618
|
(beforeId !== undefined && !Number.isInteger(beforeId)) ||
|
|
619
|
+
(archiveId !== undefined && !Number.isInteger(archiveId)) ||
|
|
587
620
|
!Number.isInteger(count) ||
|
|
588
621
|
count < 1
|
|
589
622
|
)
|
|
590
|
-
throw bad("before and
|
|
623
|
+
throw bad("before, limit and archive must be whole numbers.");
|
|
624
|
+
if (archiveId !== undefined)
|
|
625
|
+
return json(await this.#archived(room.key, archiveId, beforeId, count));
|
|
591
626
|
const page = await log.page(room.key, beforeId, count);
|
|
592
627
|
// The cards stand among the messages by the time they opened; a page that has older
|
|
593
|
-
// messages leaves the cards before its first one to the older page
|
|
628
|
+
// messages leaves the cards before its first one to the older page, and a conversation
|
|
629
|
+
// started over leaves the archived one's cards to it.
|
|
594
630
|
const first = page.messages[0];
|
|
595
631
|
const { reads, nameOf } = this.#options;
|
|
596
632
|
const self = admission.visitor.principal.id;
|
|
@@ -599,7 +635,9 @@ export class AppApi {
|
|
|
599
635
|
...page,
|
|
600
636
|
messages: withExpiredFiles(page.messages, this.#options.files),
|
|
601
637
|
cards: await cards.during(room.key, {
|
|
602
|
-
...(page.more
|
|
638
|
+
...(first && (page.more || isStartOver(first))
|
|
639
|
+
? { from: first.at }
|
|
640
|
+
: {}),
|
|
603
641
|
...(beforeId === undefined ? {} : { before: beforeId }),
|
|
604
642
|
}),
|
|
605
643
|
...(reads && nameOf
|
|
@@ -624,13 +662,19 @@ export class AppApi {
|
|
|
624
662
|
}
|
|
625
663
|
if (Object.keys(change).length === 0)
|
|
626
664
|
throw bad("Give yolo or longRunning.");
|
|
665
|
+
if (modes.peek(room.key).inheritedFrom)
|
|
666
|
+
throw bad(
|
|
667
|
+
"This room follows the modes of the conversation that made it; switch them there.",
|
|
668
|
+
);
|
|
627
669
|
const mode = await modes.set(room.key, change, owner);
|
|
628
|
-
return json({ mode:
|
|
670
|
+
return json({ mode: roomModeOf(mode) });
|
|
629
671
|
}
|
|
630
672
|
if (action === "start-over" && method === "POST" && parts.length === 4) {
|
|
631
673
|
if (room.readOnly) throw notFound();
|
|
632
674
|
await this.#owner(admission);
|
|
633
675
|
await conversations.startFresh(room.key);
|
|
676
|
+
// The other pages read the room list again: its unread count and last line changed.
|
|
677
|
+
this.#options.changed();
|
|
634
678
|
return json({ ok: true });
|
|
635
679
|
}
|
|
636
680
|
if (
|
|
@@ -653,6 +697,37 @@ export class AppApi {
|
|
|
653
697
|
throw new HttpError(404, "There is no such API.");
|
|
654
698
|
}
|
|
655
699
|
|
|
700
|
+
/**
|
|
701
|
+
* A page of the conversation a start-over line of the room ended, read only: its messages and
|
|
702
|
+
* the cards that opened within it. A line that is not one of the room's start-overs names none.
|
|
703
|
+
*/
|
|
704
|
+
async #archived(
|
|
705
|
+
room: ChannelKey,
|
|
706
|
+
until: number,
|
|
707
|
+
before: number | undefined,
|
|
708
|
+
count: number,
|
|
709
|
+
): Promise<MessagesView> {
|
|
710
|
+
const { log, cards } = this.#options;
|
|
711
|
+
const line = await log.get(room, until);
|
|
712
|
+
if (!line || !isStartOver(line)) throw notFound();
|
|
713
|
+
const page = await log.page(room, before, count, until);
|
|
714
|
+
const first = page.messages[0];
|
|
715
|
+
const previous = page.more ? undefined : await log.startOver(room, until);
|
|
716
|
+
const from = page.more
|
|
717
|
+
? first?.at
|
|
718
|
+
: previous === undefined
|
|
719
|
+
? undefined
|
|
720
|
+
: (await log.get(room, previous))?.at;
|
|
721
|
+
return {
|
|
722
|
+
...page,
|
|
723
|
+
messages: withExpiredFiles(page.messages, this.#options.files),
|
|
724
|
+
cards: await cards.during(room, {
|
|
725
|
+
...(from === undefined ? {} : { from }),
|
|
726
|
+
before: before ?? until,
|
|
727
|
+
}),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
656
731
|
/**
|
|
657
732
|
* Reads the person's rooms up to their newest message in one call: the named ones, or every room
|
|
658
733
|
* the list shows. A room they may not read is passed over as if it were not named. With `restore`
|
|
@@ -2,6 +2,7 @@ import type { SkillDocument, SkillRegistry } from "pi-roundtable";
|
|
|
2
2
|
import { SkillConflictError } from "pi-roundtable";
|
|
3
3
|
import type {
|
|
4
4
|
SkillConflict,
|
|
5
|
+
SkillDeleted,
|
|
5
6
|
SkillDocCreate,
|
|
6
7
|
SkillDocSave,
|
|
7
8
|
SkillDocView,
|
|
@@ -18,7 +19,11 @@ const MAX_SKILL_BYTES = 64 * 1024;
|
|
|
18
19
|
export interface SkillDocApiOptions {
|
|
19
20
|
skills: Pick<
|
|
20
21
|
SkillRegistry,
|
|
21
|
-
|
|
22
|
+
| "document"
|
|
23
|
+
| "saveDocument"
|
|
24
|
+
| "createDocument"
|
|
25
|
+
| "documentHistory"
|
|
26
|
+
| "delete"
|
|
22
27
|
>;
|
|
23
28
|
/** The catalog the page lists, so a name it does not list is a 404 before the registry is asked. */
|
|
24
29
|
catalog(): SkillView[];
|
|
@@ -34,7 +39,7 @@ const viewOf = (document: SkillDocument): SkillDocView => ({
|
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
41
|
* The app's REST for a skill's SKILL.md: reading it whole, saving an agent-written skill's, making a
|
|
37
|
-
* skill and listing the versions kept. The frontmatter rules are the skill tools': the registry
|
|
42
|
+
* skill, deleting an agent-written one and listing the versions kept. The frontmatter rules are the skill tools': the registry
|
|
38
43
|
* checks them, and a refusal is a 400 with its words. Every call is an owner's; a save names the
|
|
39
44
|
* hash it was edited from, and a skill that changed meanwhile answers 409 with what is there now.
|
|
40
45
|
*/
|
|
@@ -106,6 +111,11 @@ export class SkillDocApi {
|
|
|
106
111
|
throw error;
|
|
107
112
|
}
|
|
108
113
|
}
|
|
114
|
+
// The registry refuses a skill it did not write, in its own words; deleting detaches it from every carrier.
|
|
115
|
+
if (!action && method === "DELETE")
|
|
116
|
+
return json({
|
|
117
|
+
message: await skills.delete(name),
|
|
118
|
+
} satisfies SkillDeleted);
|
|
109
119
|
if (action === "history" && method === "GET" && parts.length === 4) {
|
|
110
120
|
return json({
|
|
111
121
|
versions: (await skills.documentHistory(name)).map((version) => ({
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { SkillRegistry } from "pi-roundtable";
|
|
2
|
+
import type {
|
|
3
|
+
SkillGroupApplied,
|
|
4
|
+
SkillGroupApply,
|
|
5
|
+
SkillGroupChanged,
|
|
6
|
+
SkillGroupSave,
|
|
7
|
+
SkillGroupsView,
|
|
8
|
+
SkillView,
|
|
9
|
+
} from "../api-types.ts";
|
|
10
|
+
import { json } from "../http.ts";
|
|
11
|
+
import {
|
|
12
|
+
bad,
|
|
13
|
+
bodyOf,
|
|
14
|
+
HttpError,
|
|
15
|
+
optionalList,
|
|
16
|
+
optionalText,
|
|
17
|
+
requiredText,
|
|
18
|
+
} from "./api-http.ts";
|
|
19
|
+
import type { OwnerOf } from "./team-routes.ts";
|
|
20
|
+
|
|
21
|
+
export interface SkillGroupApiOptions {
|
|
22
|
+
groups: Pick<SkillRegistry, "groups" | "setGroup" | "deleteGroup">;
|
|
23
|
+
/** The catalog the page lists: who carries each skill, and which are built in. */
|
|
24
|
+
catalog(): SkillView[];
|
|
25
|
+
/** The same change the per-skill toggle makes. */
|
|
26
|
+
attach(
|
|
27
|
+
agent: string,
|
|
28
|
+
add: readonly string[],
|
|
29
|
+
remove: readonly string[],
|
|
30
|
+
): Promise<string[]>;
|
|
31
|
+
/** Whether a knight answers; an archived or unknown one is a 404. */
|
|
32
|
+
active(agent: string): boolean;
|
|
33
|
+
changed(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The app's REST for skill groups: listing them, creating or replacing one, deleting one, and
|
|
38
|
+
* applying one to a knight. Applying switches on the group's skills the knight lacks through the
|
|
39
|
+
* per-skill toggle's path; applying it off switches off the group's skills the knight carries, built-in
|
|
40
|
+
* ones excepted. The knight carries skills, not the group.
|
|
41
|
+
* Every call is an owner's; the registry's refusals are a 400 in its words.
|
|
42
|
+
*/
|
|
43
|
+
export class SkillGroupApi {
|
|
44
|
+
readonly #options: SkillGroupApiOptions;
|
|
45
|
+
|
|
46
|
+
constructor(options: SkillGroupApiOptions) {
|
|
47
|
+
this.#options = options;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async handle(
|
|
51
|
+
request: Request,
|
|
52
|
+
parts: string[],
|
|
53
|
+
owner: OwnerOf,
|
|
54
|
+
): Promise<Response> {
|
|
55
|
+
const { method } = request;
|
|
56
|
+
const [, , name, action] = parts;
|
|
57
|
+
const { groups } = this.#options;
|
|
58
|
+
// The tier is read first, so a person below the owner learns of no group.
|
|
59
|
+
await owner();
|
|
60
|
+
if (!name) {
|
|
61
|
+
if (method !== "GET") throw new HttpError(405, "Method not allowed.");
|
|
62
|
+
return json({
|
|
63
|
+
groups: groups.groups().map(({ name, description, skills }) => ({
|
|
64
|
+
name,
|
|
65
|
+
description,
|
|
66
|
+
skills: [...skills],
|
|
67
|
+
})),
|
|
68
|
+
} satisfies SkillGroupsView);
|
|
69
|
+
}
|
|
70
|
+
const group = groups.groups().find((one) => one.name === name);
|
|
71
|
+
if (!action && method === "PUT") {
|
|
72
|
+
const body = await bodyOf(request);
|
|
73
|
+
const from = optionalText(body, "from");
|
|
74
|
+
const save: SkillGroupSave = {
|
|
75
|
+
description: requiredText(body, "description"),
|
|
76
|
+
skills: optionalList(body, "skills") ?? [],
|
|
77
|
+
...(from ? { from } : {}),
|
|
78
|
+
};
|
|
79
|
+
const renamed = save.from !== undefined && save.from !== name;
|
|
80
|
+
if (renamed) {
|
|
81
|
+
if (!groups.groups().some((one) => one.name === save.from))
|
|
82
|
+
throw new HttpError(404, "There is no such skill group.");
|
|
83
|
+
if (group) throw bad(`There is already a skill group "${name}".`);
|
|
84
|
+
}
|
|
85
|
+
let message = await groups.setGroup(name, save.description, save.skills);
|
|
86
|
+
if (renamed && save.from)
|
|
87
|
+
message = `${message} ${await groups.deleteGroup(save.from)}`;
|
|
88
|
+
this.#options.changed();
|
|
89
|
+
return json({ message } satisfies SkillGroupChanged, group ? 200 : 201);
|
|
90
|
+
}
|
|
91
|
+
if (!group) throw new HttpError(404, "There is no such skill group.");
|
|
92
|
+
if (!action && method === "DELETE") {
|
|
93
|
+
const message = await groups.deleteGroup(name);
|
|
94
|
+
this.#options.changed();
|
|
95
|
+
return json({ message } satisfies SkillGroupChanged);
|
|
96
|
+
}
|
|
97
|
+
if (action === "apply" && method === "POST" && parts.length === 4) {
|
|
98
|
+
const body = await bodyOf(request);
|
|
99
|
+
const apply: SkillGroupApply = {
|
|
100
|
+
agent: requiredText(body, "agent"),
|
|
101
|
+
on: body.on !== false,
|
|
102
|
+
};
|
|
103
|
+
if (!this.#options.active(apply.agent))
|
|
104
|
+
throw new HttpError(404, "There is no such room, knight or file.");
|
|
105
|
+
const catalog = new Map(
|
|
106
|
+
this.#options.catalog().map((entry) => [entry.name, entry]),
|
|
107
|
+
);
|
|
108
|
+
// A built-in skill is carried always: it is never added nor switched off.
|
|
109
|
+
const switchable = group.skills.filter(
|
|
110
|
+
(skill) => catalog.get(skill)?.source.kind !== "builtin",
|
|
111
|
+
);
|
|
112
|
+
const carries = (skill: string) =>
|
|
113
|
+
catalog.get(skill)?.carriers.includes(apply.agent) ?? false;
|
|
114
|
+
const added = apply.on
|
|
115
|
+
? switchable.filter((skill) => !carries(skill))
|
|
116
|
+
: [];
|
|
117
|
+
const removed = apply.on ? [] : switchable.filter(carries);
|
|
118
|
+
if (added.length > 0 || removed.length > 0) {
|
|
119
|
+
await this.#options.attach(apply.agent, added, removed);
|
|
120
|
+
this.#options.changed();
|
|
121
|
+
}
|
|
122
|
+
return json({ added, removed } satisfies SkillGroupApplied);
|
|
123
|
+
}
|
|
124
|
+
throw new HttpError(404, "There is no such API.");
|
|
125
|
+
}
|
|
126
|
+
}
|