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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,54 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.26.0] - 2026-10-30
|
|
9
|
+
|
|
10
|
+
The core peer range is now `>=0.26.0 <0.27.0`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- A card or an unprompted reply in a host room no longer pushes to every owner's phone; host rooms reach phones only through notices.
|
|
15
|
+
- A file an agent attaches in a host room is bound to the room and the kind's file retention, so its readers can download it (before, only owners could, and the 30-day agent sweep applied).
|
|
16
|
+
- Answering a card in a host room asks the kind's `access` with the person's roles, so a role-gated reader is no longer refused or dropped from the room.
|
|
17
|
+
- A link with a tab, newline or space is no longer taken for a path of this origin.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- A running turn's strip shows only its three newest tool calls; a button counts the earlier ones and opens them all (and folds them again).
|
|
22
|
+
|
|
23
|
+
## [0.25.0] - 2026-10-29
|
|
24
|
+
|
|
25
|
+
The core peer range is now `>=0.25.0 <0.26.0`.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Make, archive and arrange agents. The **+** button of the room list and of the switcher drawer opens a menu: **New agent**, **New group** and **Archived**. New agent takes a name, the permanent name (`^[a-z0-9-]{1,32}$`, filled in from the name until typed by hand), the prompt, a model, an optional opening task and a look (the one its role suggests, or one picked in the character creator, which opens in place of the form), and opens the new agent's room. A bad or taken name, an unusable model, a bad look field (`400 {error, field}`) and an unknown skill are refused with the agent server's words before anything is made.
|
|
30
|
+
- Archive and restore. **Archive agent** and **Archive group** are in the agent sheet, the room's ⋯ menu and the long-press menu of a row (not on the coordinator's room, which cannot be archived), and ask first, naming what they archive. **Archived** lists archived agents and groups with Restore and Delete; Delete is only for an archived one, asks first and names it, frees the name, and clears the room's messages, cards, mute and modes in this app, and the dispatch threads under it with their messages and cards. Restoring an agent does not put it back in its groups; a group is restored only while all its members are active.
|
|
31
|
+
- Skills. The agent sheet lists the host's skills (the Skills pane's catalog when the host supplies one, else the skills addon's) with a switch each, applied as it is flipped. Built-in skills stay on; a host without the skills addon shows no section.
|
|
32
|
+
- Groups. **New group** and **Edit group** (the group's header name, its ⋯ menu, its row's menu) take a name, 2 to 6 members in the order picked, and the host; Edit sends only what changed.
|
|
33
|
+
- New owner-only routes, with the tier read again, the console `Origin` on every write, `404` for an agent, group or room that does not exist and `400` for a bad body or what the agent server refuses: `POST api/agents` (`201`, the profile), `POST api/agents/<name>/archive`, `POST api/agents/<name>/restore`, `DELETE api/agents/<name>`, `GET` and `PUT api/agents/<name>/skills` (`{add?, remove?}`, one change), `POST api/groups` (`201`), `GET`, `PATCH` and `DELETE api/groups/<name>`, `POST api/groups/<name>/archive` and `restore`, and `GET api/archive`. They call `AgentTeam.createAgent`, `archive`, `restore`, `remove`, `createGroup` and `updateGroup` and the skill registry's `attach`, and tell the open pages to read the room list again. Needs `pi-roundtable` with those methods.
|
|
34
|
+
- The new sheets (the new agent form, the group editor and the archive) are one chunk that loads when first opened; the creator takes an `onPick` mode that hands back the look instead of saving it.
|
|
35
|
+
- Main prompts. **Main prompts** (a link in Settings, shown only on a host with prompt documents) lists the documents the host registered through `PROMPTS` (`shared` and `guest` from `prompts.*`, and a plugin's own, such as a persona), each with a "Differs from the file version" badge and who set the override and when. One opens in a full-screen sheet with the text in force, **Review changes** (a line diff against what the text was read as, nothing is saved before it), **Save**, **History** (each change newest first, read against the version before it, **Use this text** takes it into the editor to be reviewed again) and **Revert to the file version**, which asks first and names the document. A save sends the text it was edited from (`base`); when the document changed meanwhile the server answers `409 {error, conflict: "changed", current}`, and the page reads the diff again against the text there now and keeps the draft. The override applies from the agent's next turn.
|
|
36
|
+
- Agent prompt history. The agent sheet gains **History** for the prompt: the versions of that agent's prompt, who changed it and when, and **Use this text**, which puts a version into the editor to be reviewed like any edit. An agent has no override layer; its saved prompt is the text in force. A prompt change from the sheet is kept under the owner's name.
|
|
37
|
+
- Skills editing. The Skills page edits an agent-written skill's whole SKILL.md (frontmatter and body) in a full-screen sheet, with the same frontmatter and size rules as the skill tools (the server's words are shown), a diff to read before **Save**, a history of versions and a `409` that reads the diff again against the version there now (the file is read with its SHA-256 hash and saved with it as `baseHash`). **New skill** starts from a template, reads as an all-added diff and creates the skill, attached to no agent. A built-in, package or linked skill shows a lock and where it comes from instead of **Edit**. Deleting a skill is not offered: the registry has no safe delete, and an agent still carrying one would lose it silently. A page without the owner app keeps the old hint.
|
|
38
|
+
- New owner-only routes, with the tier read again, the console `Origin` on every write, `404` for a prompt or skill that does not exist and `400` for a bad body or what the host refuses: `GET api/prompts` (`{documents}`; a document whose file version cannot be read has `broken`, the reason, and the others are listed as usual; the page marks it **File version cannot be read** and shows the reason instead of an editor), `GET` and `PUT api/prompts/<id>` (`{text, base}`; `409` with `current` when `base` is not the text in force), `POST api/prompts/<id>/revert` (`{base}`, with the same `409`), `GET api/prompts/<id>/history`, `GET api/agents/<name>/prompt-history`, `GET` and `PUT api/skill-docs/<name>` (`{text, baseHash}`), `POST api/skill-docs` (`{text}`; `201`) and `GET api/skill-docs/<name>/history`. `GET api/session` carries `prompts: true` when the host has prompt documents and `skillEdit: true` when it has the skills addon; the flags follow the host only, and each route reads the tier again and answers `403` below the owner. The body of a prompt write may be as large as the document's `maxChars` needs for the new text and for `base` (6 bytes a character at most), more than a plugin's roomy document would fit in a fixed 128 KiB; a larger body is `413`. Needs `pi-roundtable` with `PROMPTS` and the skill registry's documents.
|
|
39
|
+
- The editors (prompts and skills) are chunks that load when first opened; the page's first load does not grow.
|
|
40
|
+
- Pins and folders for the room list. The owners arrange it together, one layout for the whole app that every device shows: Home first, the pinned rooms in the owners' order, the owners' folders in theirs, and the rest (Ungrouped) by latest activity. A room is pinned or in one folder, not both. A room's menu (and the room header's ⋯) gains Pin or Unpin, Move to folder… and, in the list, Move up or Move down; a folder's header folds (per device, in `localStorage`; the folder with the open room shows open, and a folded folder still shows its rooms' waiting cards and working dots) and has a menu to rename, mute or unmute the whole folder, step and delete (after a question; its rooms go to Ungrouped); the list's ⋯ adds a folder and turns on arrange mode, which shows a ⋯ beside each row. The switcher drawer follows the same order with a heading for each folder.
|
|
41
|
+
- Reordering by hand: a finger that rests on a row lifts it, and dragging reorders it within its list or moves it into another folder, the pinned list or Ungrouped (a folded folder opens after the finger rests on it); a held folder header moves the folder. The rows make room, the list scrolls near its edges, the drop place is outlined, and Escape or a drop outside every list cancels. A mouse drags once it moves with the button down; a touch within 32 px of a screen edge never lifts a row. Ungrouped stays by activity, so a row dragged within it goes back. The lists keep their order while a row is lifted, the pinned list and empty folders appear as places to drop, and on Android the browser's own long press is the hold. The arranged list, its folders and styles, the lift code (hand-rolled on pointer events, about 5 KB minified) and the drawer's rows are chunks of their own, so a page with nothing arranged downloads next to nothing for them; Move up and Move down are the keyboard way.
|
|
42
|
+
- `GET api/rooms` also answers `pins` and `folders`; new owner-only routes `PUT rooms/<key>/pin`, `PUT rooms/<key>/folder`, `POST api/folders`, `PATCH` and `DELETE api/folders/<id>`, and `PUT api/folders/<id>/mute`, which mutes or unmutes every room of a folder through the rooms' own mutes in one step. Each write rereads the person's tier, needs the console's own `Origin`, takes one advisory lock, answers with the whole layout and sends `changed` to the other pages. Three additive tables, `web_room_folders`, `web_room_folder_rooms` and `web_room_pins`, in the new migration `web-room-layout`; nothing is dropped, and a room that no longer exists keeps its place and is not shown. Types `FolderView` and `LayoutView`, and `RoomsView` extends the latter.
|
|
43
|
+
- Deleting an agent or a group also drops its pin or folder place.
|
|
44
|
+
- Who sits at the round table. A **Who's at the table** button in the page's header opens a sheet (a bottom sheet under 720 px, a dialog wider) that lists every agent with its picture and a switch. Taking an agent off the table is muting its room's push (`PUT rooms/<key>/mute`, shared by every owner and device) and putting it back is unmuting it, so the table, the agent sheet and the room menu share one switch: the table seats exactly the agents whose room is not muted, and muting a room anywhere takes its agent off the table. **Leave the table** is also in a seat's menu (long press, right click or the Menu key; the scene's new `onSeatMenu` option and `seatAt`) and in the dialogue box's ⋯ button. The table re-seats at once and keeps the camera still unless it was on the agent who left, when it returns to `#/table` in place. With everyone off the table the page shows an empty state with the sheet's button; an off-table agent stays in the rooms list with its bell-slash; a group room's table still seats all its members. Traditional Chinese for the new wording.
|
|
45
|
+
- Leaving and joining play a short show in the hall's style. A leaving seat squashes and vanishes in flat, outlined smoke clouds that drift up with a few sparkles (about 0.7 s), the others glide to their new places, and a joining seat rises out of a small rune circle with a bounce and a column of light (about 0.85 s). The pictures are drawn on small canvases and disposed when the show ends. Under reduced motion the picture only dips and returns; the 2D fallback fades in over a ring and out inside a puff with CSS. A seat's picture is kept across rings, so the others do not flicker while the ring is rebuilt.
|
|
46
|
+
- Traditional Chinese for the new wording.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- The search palette (⌘K) and the schedules pane are lazy chunks, and the question and sheet hosts (agent, team, confirms) share one, so the page loads them after the first screen instead of with it.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- The round table's overview keeps every seat and its name in the picture on a phone. It left half a name's width clear of the sides (the names are page text, about 44 px each side of a seat) and counted the standee's emphasis strokes, which stand wider than its board; a window resized during a camera turn now ends that turn on the new picture's overview.
|
|
55
|
+
|
|
8
56
|
## [0.24.2] - 2026-10-29
|
|
9
57
|
|
|
10
58
|
- Release in lockstep with pi-roundtable 0.24.2; no package-specific behavior changes. The core peer range stays `>=0.24.0 <0.25.0`.
|
package/README.md
CHANGED
|
@@ -20,15 +20,17 @@ Then only a person whose principal holds the owner role gets in.
|
|
|
20
20
|
## Contents
|
|
21
21
|
|
|
22
22
|
- [Requirements](#requirements) and [Install](#install)
|
|
23
|
-
- [The owner app](#the-owner-app): rooms, cards, [modes](#modes-yolo-and-long-running), [dispatch threads](#dispatch-threads), [uploads](#uploads), [characters](#characters-in-the-page), [the Camelot look](#the-camelot-look), [the round table](#the-round-table), [the files pane](#the-files-pane)
|
|
23
|
+
- [The owner app](#the-owner-app): rooms, cards, [modes](#modes-yolo-and-long-running), [dispatch threads](#dispatch-threads), [pins and folders](#pins-and-folders), [uploads](#uploads), [characters](#characters-in-the-page), [the Camelot look](#the-camelot-look), [the round table](#the-round-table), [the files pane](#the-files-pane)
|
|
24
|
+
- [Members and the gate](#members-and-the-gate): [who is let in](#who-is-let-in), what each tier sees, and the steps owed before the app opens
|
|
24
25
|
- [Install on a device](#install-on-a-device) and [Notifications](#notifications)
|
|
25
26
|
- [Authentication](#authentication) and the [threat model](#threat-model)
|
|
26
27
|
- [Options](#options), [the console's panes](#the-consoles-panes) and [host feature ports](#host-feature-ports)
|
|
28
|
+
- [Host rooms, pages and cards](#host-rooms-pages-and-cards): rooms a host plugin owns, with read marks
|
|
27
29
|
- [App protocol](#app-protocol), [Screenshots](#screenshots) and [Development](#development)
|
|
28
30
|
|
|
29
31
|
## Requirements
|
|
30
32
|
|
|
31
|
-
- Bun 1.4.4 or later, and pi-roundtable `>=0.
|
|
33
|
+
- Bun 1.4.4 or later, and pi-roundtable `>=0.26.0 <0.27.0` as a peer dependency.
|
|
32
34
|
- The host must run on the machine that holds its data directory: the console reads `<dataDir>/sessions` from disk, and the app keeps its files and keys under `<dataDir>/web/`.
|
|
33
35
|
- An agent team: `agents` in the configuration, with or without `discord`. Without Discord the host also needs a surface that hosts rooms, and this plugin is one; the start fails otherwise, naming the fix.
|
|
34
36
|
- A way to authenticate the owners (see [Authentication](#authentication)): a password, Cloudflare Access or a verifier of your own in front of the listener, or `none` on a listener only the owner reaches.
|
|
@@ -104,14 +106,15 @@ The page is built for a phone first: a bottom tab bar under 720 px, a side rail
|
|
|
104
106
|
|
|
105
107
|
| Where | What it does |
|
|
106
108
|
|---|---|
|
|
107
|
-
| **Rooms** | Home first, then
|
|
109
|
+
| **Rooms** | Home first, then the pinned rooms, the owners' folders and the rest by latest activity, each with its dispatch threads, with badges for working, a card waiting, yolo and long-running (see [Pins and folders](#pins-and-folders)). |
|
|
108
110
|
| **A room** | Each message with its author's picture and name, Markdown (raw HTML is shown as text), files, and a live progress strip with the streamed text and tool chips. A sticky composer with upload and Stop. The header menu holds the mode switches, Profile and Start over. |
|
|
109
111
|
| **Cards** | Inline in the room: an approval with the call's details and a copy button; a question with options, an "Other" field and Cancel; a questionnaire; and the card at a turn's time limit. Answered cards show the answer given. |
|
|
110
|
-
| **Agent
|
|
112
|
+
| **Agent sheet** | Display name, prompt (with its History), model and thinking level, the skills it carries, Notifications, the character creator (see [Characters in the page](#characters-in-the-page)) and Archive. See [Making and arranging agents](#making-and-arranging-agents). |
|
|
111
113
|
| **Schedules** | Every schedule of the team, labelled by room or channel, with a confirmed cancel. |
|
|
112
114
|
| **Table** | The round table (see [The round table](#the-round-table)). |
|
|
113
115
|
| **Files** | The agents' workspace: a tree, highlighted files and an editor (see [The files pane](#the-files-pane)). |
|
|
114
|
-
| **
|
|
116
|
+
| **Main prompts** | The prompt documents of the host (shared, guest, a plugin's own) with their file versions, overrides and history (see [Editing prompts and skills](#editing-prompts-and-skills)). From Settings. |
|
|
117
|
+
| **Settings** | Color scheme and look, the link to Main prompts, install, notifications for this device, and Log out (password mode). |
|
|
115
118
|
|
|
116
119
|
**Sending and stopping.** A message goes to the agent as the owner's. **Stop** ends the turn.
|
|
117
120
|
A message that cannot go shows "Not sent" with Try again and Discard; a lost connection shows a banner under the header and the page reconnects with backoff, then fetches what it missed.
|
|
@@ -146,13 +149,52 @@ The app hosts the core's dispatch threads (`ChatSurface.threads`). A background
|
|
|
146
149
|
|
|
147
150
|
A thread's room is read only: a `send` to it is refused with `read_only`, and it has no modes. Its cards are the dispatch's own, such as a coding job's approvals, and any owner answers them there as in any room. Agents post in it under their own names. A closed thread stays readable (the list keeps the 30 newest closed ones), and the host closes the threads a restart left open, with a restart notice. `GET api/rooms` lists a thread with `parent` and, once closed, `closed: true`; opening and closing one sends `changed`.
|
|
148
151
|
|
|
152
|
+
|
|
153
|
+
### Making and arranging agents
|
|
154
|
+
|
|
155
|
+
An owner changes the team from the app. Everything goes through the agent server's own operations (`AgentTeam`), the ones the `agent_create`, `archive` and `group_*` tools use, so an agent made here is the one an agent would make, and an archived one is the one the `archive` tool leaves.
|
|
156
|
+
|
|
157
|
+
- **New agent.** The **+** button of the room list and of the drawer opens a menu with New agent, New group and Archived. New agent asks for a name, the permanent name (lowercase letters, digits and dashes, which names the room and cannot be changed; it follows the name until typed by hand), the prompt, a model, an optional opening task and a look. The look is the one its role suggests until the creator picks another. On success the new agent's room opens. A bad or taken name, an unusable model, a bad look and an unknown skill are refused with the agent server's words before anything is made.
|
|
158
|
+
- **Archive and restore.** Archive is in the agent sheet and in the ⋯ menu and long-press menu of a room (the coordinator's room has none). It asks first, naming the agent or group. An archived agent stops answering, its schedules are cancelled and it leaves its groups; its history is kept. **Archived** in the **+** menu lists what is archived with Restore and Delete. Restoring an agent does not put it back in its groups; restoring a group needs all its members active, otherwise it is refused and stays archived.
|
|
159
|
+
- **Delete.** Only an archived agent or group can be deleted, from the archive, after a question that names it. Deleting frees the name, removes the agent's group cursors and detaches its skills, and clears what this app kept of the room (its messages, its cards, its mute and its modes). A Discord channel, if the agent had one, stays under Archive with its history, since Discord is never asked to delete a channel.
|
|
160
|
+
- **Skills.** The agent sheet lists the host's skills (the Skills pane's catalog when the host supplies it, else the skills addon's own) with a switch each, applied as it is flipped and from the agent's next turn. Built-in skills are carried by every agent and stay on. A host without the skills addon shows nothing.
|
|
161
|
+
- **Groups.** New group and Edit group (the group's header name, its ⋯ menu and its row menu) take a name, 2 to 6 members in the order they are picked, and the host who answers when nobody else fits; Edit sends only what changed. Archive group asks first.
|
|
162
|
+
|
|
163
|
+
All of it is for owners: the owner's tier is read again on each call, a change needs the console's `Origin`, and a room or agent that does not exist is a 404. Every change tells the open pages to read the room list again.
|
|
164
|
+
|
|
165
|
+
### Editing prompts and skills
|
|
166
|
+
|
|
167
|
+
An owner edits the texts that shape the agents from the app. Both editors are full-screen sheets that load when first opened, and in both nothing is saved before **Review changes** has shown the lines that change.
|
|
168
|
+
|
|
169
|
+
- **Main prompts.** Settings links to the list (on a host that has prompt documents). Each document is a text the host reads from a file or the configuration, with an override the owner stores in the app: the core's `shared` (the base of every agent's prompt) and `guest` (used when the speaker is not the owner), and any a plugin registers through `PROMPTS`. A document that differs from its file version has a badge with who set it and when. **History** lists each change, newest first, and a version can be read against the one before and taken into the editor. **Revert to the file version** asks first, names the document and drops the override. The override applies from the agent's next turn, with no restart, and the host logs a warning at start for each document that differs from its file. Saving sends the text the sheet was edited from; if someone changed the document meanwhile, the sheet reads the diff again against the new text and keeps the draft.
|
|
170
|
+
- **Agent prompts.** An agent's prompt is edited in its sheet as before; **History** there lists the earlier versions and who saved them. There is no override layer for an agent: the saved prompt is the text in force.
|
|
171
|
+
- **Skills.** The Skills page has **Edit** on a skill an agent wrote and **New skill**. The sheet holds the whole SKILL.md, frontmatter included, checked by the same rules as the skill tools; a refusal is shown in the server's words. A save is checked against the hash the file was read with (`409` and a new diff when it changed), and each save is kept as a version in **History**. A built-in, package or linked skill shows a lock and where it comes from, and is changed there. Deleting a skill is not offered here.
|
|
172
|
+
|
|
173
|
+
All of it is for owners: the tier is read again on each call, a change needs the console's `Origin`, and a prompt or skill that does not exist is a 404.
|
|
174
|
+
### Pins and folders
|
|
175
|
+
|
|
176
|
+
The owners arrange the room list together: one layout for the whole app, kept on the server and shown the same on every owner's phone and desktop. The order is **Home** (always first), the **pinned** rooms in the owners' order, the owners' **folders** in theirs, and **Ungrouped**, which holds the rest by latest activity. Until there is a folder, the rest keeps its Agents and Groups headings. A room is in one place: pinning a room takes it out of its folder and putting it in a folder unpins it. Home and dispatch threads (which follow their room) cannot be pinned or filed. The room switcher drawer on a phone lists the rooms in the same order, with a heading for each folder, and the rooms with a card waiting stay on top.
|
|
177
|
+
|
|
178
|
+
| Where | What it does |
|
|
179
|
+
|---|---|
|
|
180
|
+
| A room's menu (long press, right click, or the Menu key; the **⋯** beside a row in arrange mode; or the **⋯** in the room's header) | **Pin** or **Unpin**, **Move to folder…** (a sheet with the folders, Ungrouped and **New folder…**), and **Move up** or **Move down** within the pinned list or the folder. |
|
|
181
|
+
| A folder's header | Fold or unfold it (kept per device in `localStorage`, `roundtable-console:folders-collapsed`; the folder that holds the open room shows open, and a folded folder still shows its rooms' waiting cards and working dots). Its **⋯** (or a long press, right click or the Menu key) offers **Rename**, **Mute the whole folder** or **Unmute the whole folder**, **Move up**, **Move down** and **Delete folder** (after a question; its rooms move to Ungrouped, no room is deleted). A bell-slash shows when every room in it is muted, a fainter one when some are. |
|
|
182
|
+
| The list's **⋯** | **Add a folder**, and **Arrange**, which shows a **⋯** beside every row and a bar to add a folder until **Done**. |
|
|
183
|
+
| A room row or a folder header, held or dragged | A finger that rests on a row lifts it (a short vibration where the phone has one); dragging then reorders it within its list, moves it into another folder, the pinned list or Ungrouped, and a folded folder opens after the finger rests on it. A held folder header moves the whole folder among the folders. The rows make room as you drag, the list scrolls near its edges, the place it lands is outlined, and Escape or a drop outside every list puts everything back. Letting go without moving opens the row's menu as before. A mouse lifts a row once it moves with the button down. A touch within 32 px of a screen edge never lifts a row (the system keeps that edge), so the menu is the way there. While a row is lifted the lists keep their order, so a message that arrives does not move rows under the finger, and the pinned list and empty folders show as places to drop even when they hold nothing yet. On Android the browser's own long press (its `contextmenu`, at about 400 ms) counts as the hold. Ungrouped is ordered by activity, so a row dragged within it goes back; pin it or put it in a folder to place it. The lift code is a chunk of its own that loads on the first press. |
|
|
184
|
+
|
|
185
|
+
Folders have 1 to 40 characters (code points) in their name, no control characters, a name that no other folder has (case does not matter), and there are at most 50. The routes are owner-only, as the mute route is: the person's tier is read again, a write needs the console's own `Origin`, and `GET api/rooms` carries the layout with the rooms so the two never disagree. A stored place of a room that is gone (an archived agent) is kept and not shown, so the room returns to it when it comes back. Folder mute is the rooms' own mute (`web_push_muted_rooms`) set for every room in the folder at once, and a room moved into the folder later keeps its own.
|
|
186
|
+
|
|
187
|
+
Until the owners arrange something, the page is the plain list it always was; the arranged list (folders, the lift, their styles) is a set of chunks that load once there is a pin or a folder, or when the list's **⋯** is used, and the drawer's rows load with the drawer, so the first load costs almost nothing for the feature.
|
|
188
|
+
|
|
189
|
+
The layout lives in three additive tables created by the plugin's migration `web-room-layout`: `web_room_folders`, `web_room_folder_rooms` and `web_room_pins`. Every write takes one advisory lock and rewrites the positions densely, so two devices editing at once leave no gap and no duplicate.
|
|
190
|
+
|
|
149
191
|
### Uploads
|
|
150
192
|
|
|
151
193
|
An owner may send a file of any kind in a room, up to four to a message: the button, a paste (a copied image, say) or a drop. `POST <mount>/api/uploads` takes one file as `multipart/form-data` (field `file`) and answers `{id, name, type, size}`; the message then names it in `uploads`. A long text goes as a file too: the page turns a paste of 4,000 characters or more into a `pasted-text-<n>.txt` chip, and a message over 32,000 characters into `message.txt` with the line "(long text attached)"; the server still refuses a text over 32,000 characters.
|
|
152
194
|
|
|
153
195
|
- **Streamed, not buffered.** The body is read as a stream and written to disk as it arrives, so a large file costs disk, not memory. Only the first bytes are kept to tell what the file is. A file over `uploadMaxBytes` is refused with `413` (before it is read when the length says so, as it passes the limit otherwise), and nothing is left on disk.
|
|
154
196
|
- **Any type.** Images are checked by their first bytes (a file that says it is a PNG but is not one is refused with `415`); a file that is valid UTF-8 without NUL bytes is kept as text (`text/markdown`, `text/csv` or `application/json` by its name, else `text/plain`); a PDF by its first bytes keeps `application/pdf`; everything else is kept as `application/octet-stream` under the name it came with. What the browser said the type was (often empty, or `video/mp2t` for a `.ts` file) does not decide.
|
|
155
|
-
- **Bounded.** A person may have 16 files and 400 MiB waiting for a message, counting uploads on their way (`429` beyond that); a waiting file is deleted after a day, a sent one after 30. The page deletes a chip's upload when it is removed or its send fails (`DELETE <mount>/api/uploads/<id>`, the owner's own waiting uploads only), and a body that is cut short leaves no file behind.
|
|
197
|
+
- **Bounded.** A person may have 16 files and 400 MiB waiting for a message, counting uploads on their way (`429` beyond that); a waiting file is deleted after a day, a sent one after `uploads.keepDays` (30 by default). The page deletes a chip's upload when it is removed or its send fails (`DELETE <mount>/api/uploads/<id>`, the owner's own waiting uploads only), and a body that is cut short leaves no file behind.
|
|
156
198
|
- **Served safely.** A stored file is a download, never a page (`content-disposition: attachment`, `content-security-policy: sandbox`, `nosniff`); only the four image types are shown inline.
|
|
157
199
|
- **What the agent gets.** The turn's message names each file, and the host saves it for the agent's tools: the app hands the host the stored file's path, so a large file is copied from disk, never held in memory. The agent takes a file up to `uploadMaxBytes` (the surface's `maxAttachmentBytes`), not the 25 MiB a Discord attachment is held to. Images (four at most) are also shown to the model, and a text or PDF file can be read by the agent. A file over 25 MiB is kept as a file for tools: it is not shown to the model and `read_attachment` refuses it, and the turn's attachment list marks it "too large for read_attachment; kept as a file for tools", which a tool that takes files opens as it is.
|
|
158
200
|
- **Limits in front of the app.** The upload route asks the host's listener for a request body of `uploadMaxBytes` plus 64 KiB of form framing, so a larger `uploadMaxBytes` raises the listener's limit past its 128 MiB default (a listener the host fixed with its own `maxRequestBodyBytes` decides instead, and refuses a larger body with `413`). A reverse proxy has its own limit; Cloudflare's free plan caps request bodies at 100 MB, so a larger file needs a paid plan or an upload path that skips the proxy.
|
|
@@ -178,7 +220,7 @@ Every agent is drawn as its composed character (`pi-roundtable/characters`). The
|
|
|
178
220
|
|
|
179
221
|
### The round table
|
|
180
222
|
|
|
181
|
-
The `table` pane is a council hall: a ring of tall dark-wood podiums, one for each active agent (the coordinator first) and one low lip at the near edge for you, under torches, banners and a stained-glass window, around a glowing rune circle. The hall is drawn in the characters' own flat, outlined style (a stepped tone gradient and a dark line round every shape, no photographic textures). Dark is the hall at night, light the same hall by day; the Modern and Camelot looks change the banners and the trim, not the hall. Each agent stands behind its podium as a waist-up standee: the character without its frame, cut out with a thin light rim, a dark line and a visible board thickness, turning to the camera within limits. A seat's state comes from the same rules as the room list (`sleepy` sits as `idle`): a thinking seat shows a "…" badge and bobs, a seat with a card waiting a gold "!", a speaking seat has a light on its podium and the concept art's three gold emphasis strokes beside its head; a change of face redraws that one picture once it has stood for 300 ms. More than twelve agents show twelve and a "+N" podium that opens the list.
|
|
223
|
+
The `table` pane is a council hall: a ring of tall dark-wood podiums, one for each active agent whose room is not muted (the coordinator first) and one low lip at the near edge for you, under torches, banners and a stained-glass window, around a glowing rune circle. The hall is drawn in the characters' own flat, outlined style (a stepped tone gradient and a dark line round every shape, no photographic textures). Dark is the hall at night, light the same hall by day; the Modern and Camelot looks change the banners and the trim, not the hall. Each agent stands behind its podium as a waist-up standee: the character without its frame, cut out with a thin light rim, a dark line and a visible board thickness, turning to the camera within limits. A seat's state comes from the same rules as the room list (`sleepy` sits as `idle`): a thinking seat shows a "…" badge and bobs, a seat with a card waiting a gold "!", a speaking seat has a light on its podium and the concept art's three gold emphasis strokes beside its head; a change of face redraws that one picture once it has stood for 300 ms. More than twelve agents show twelve and a "+N" podium that opens the list.
|
|
182
224
|
|
|
183
225
|
- **Cameras.** The overview stands behind your podium, raised. A drag or swipe turns it round the ring and snaps to a podium; the wheel or a pinch moves in on the podium across the ring and back out. A close-up of a speaker stands low inside the ring, tilted a little, with thin speed lines flashing for 400 ms on a new speaker. A turn follows the ring the shorter way and takes 450 to 650 ms. A swipe never starts within 32 px of the screen's edge. Under `prefers-reduced-motion` the camera cuts, with no speed lines, bob or flicker.
|
|
184
226
|
- **Talking in the scene.** A tap on a podium (or on its name, a button over the plate) turns the camera to that agent and slides up a dialogue box on the agent's own room: a nameplate tab in the agent's color with its picture, the line being said (Markdown, typed out while it streams, a tap shows it whole), the cards waiting for an answer, and the room's own message box, with its unsent text kept per room. **Log** opens the room's whole message list as a sheet over the scene. The address is `#/table/<agent>` (the agent's name or the key of its room): Back returns to the whole ring, a link opens the scene at that agent, and an address that names nobody seated goes back to `#/table`.
|
|
@@ -186,6 +228,8 @@ The `table` pane is a council hall: a ring of tall dark-wood podiums, one for ea
|
|
|
186
228
|
- **Phones.** Held upright, the scene fills the top 55 % and the dialogue box the rest, and the box collapses to its nameplate line; on its side the scene is whole and the box lies over its bottom third. The seats are also listed as buttons with more than ten seats.
|
|
187
229
|
- **Fallback.** Without WebGL (or after a lost context) the same ring is drawn as SVG in the same palette, with the same standees, plates and dialogue box, never a blank pane. `?nogl` in the page address (`/console/?nogl`, or `#/table?nogl`) draws it on a browser that has WebGL, to try that path; `nogl=0` and `nogl=false` leave the choice to the browser.
|
|
188
230
|
- **Performance.** The WebGL scene is a chunk of its own, fetched when the view first opens and cached by the service worker, never precached. It stops drawing while the page is hidden or the table is off screen, draws a few frames a second while nothing moves (so a torch flickers without keeping the GPU busy), and caps the pixel ratio at 2 (1.5 on phones).
|
|
231
|
+
- **Who sits.** With many agents the table can be thinned out. **Who's at the table** (a button in the page's header) opens a sheet, a bottom sheet on a phone and a dialog elsewhere, that lists every agent with its picture and a switch; an agent switched off leaves the table, and switched on sits down again. Leaving the table *is* muting the agent's room push (the per-room mute, `PUT rooms/<key>/mute`, shared by every owner and device), so there is one switch behind the table, the agent sheet and the room menu: the table seats exactly the agents whose room is not muted, and muting a room anywhere takes its agent off the table. A seat's menu (a long press, a right click or the Menu key) and the ⋯ button of the dialogue box also offer **Leave the table**. An agent off the table stays in the rooms list with its bell-slash, and the table re-seats at once, the camera staying where it was unless it was on the agent who left, when it goes back to the whole ring (`#/table`, in place). With everyone off the table the page says so and offers the sheet. A group room's table is not affected: it seats all its members, muted or not.
|
|
232
|
+
- **Leaving and joining.** An agent who leaves goes in a manga-style puff: the standee squashes a little, flat outlined clouds pop up round it and hide it, then drift up and fade with a few sparkles (about 0.7 s). The other seats then glide to their new places with the camera. An agent who sits down is called up out of a small rune circle in the hall's teal that glows on the floor at its new place; the podium rises out of it with a slight bounce, under a column of light (about 0.85 s), once the others have opened the gap. The pictures are drawn on small canvases in the hall's style and given back when the show ends. Under `prefers-reduced-motion` the picture only dips and comes back (0.16 s each way), with no smoke or rise; the 2D fallback fades the seat in over a ring and out inside a puff with CSS.
|
|
189
233
|
- **Group rooms.** A group room's header has a "Table" button that switches the room between its list and the scene; the choice is kept per room. In the scene the whole conversation runs in the dialogue box, the camera turns to whoever replies, and it rests on the whole ring while you speak; a member that starts thinking shows its badge wherever the camera is, and a speaker change never takes the focus from your message box. Under the Camelot look a compact strip of the members stands under the header in the list, with the speaker lit.
|
|
190
234
|
|
|
191
235
|
### The files pane
|
|
@@ -245,6 +289,48 @@ A host that adds a root the agents cannot reach should authenticate with a verif
|
|
|
245
289
|
The REST is `GET api/workspace`, `GET api/workspace/<place>/list?path=`, `file?path=` (the text, `hash`, `editable` and, when not, `reason`), `raw?path=` (bytes, `no-store`, `&download=1` forces a download), `PUT file?path=` (`{ text, baseHash }`), `POST new?path=` (`{ kind: "file" | "dir", text? }`), `POST rename` (`{ from, to }`), `DELETE entry?path=`, `GET trash`, `POST trash/<id>/restore` and `DELETE trash/<id>`.
|
|
246
290
|
The service worker never caches `api/workspace`.
|
|
247
291
|
|
|
292
|
+
## Members and the gate
|
|
293
|
+
|
|
294
|
+
By default the app lets in only owners, as it always has. With `admission: "access"` it lets in everyone the host's `IDENTITY` admits on the app's surface: owners, admins and members. The app never makes a rule of its own about who that is; it asks `IDENTITY`, whose `access` configuration decides.
|
|
295
|
+
|
|
296
|
+
### Who is let in
|
|
297
|
+
|
|
298
|
+
`admission: "access"` needs a credential that names a person: `auth: { mode: "oidc", ... }` (below), or a verifier that reports who signed in. `none` and `password` speak for one shared actor and are refused with it.
|
|
299
|
+
|
|
300
|
+
- A person whose identity is linked to a principal is looked up with `IDENTITY.peek`, which writes nothing. A person nobody has linked is admitted through `IDENTITY.resolve`, which makes a new principal only where `access.provisioning.app` is `admitted` for the surface; otherwise they are refused.
|
|
301
|
+
- The facts the app reports are partial (`partial: true`): signing in here shows less than a person may hold elsewhere, and never lowers a tier recorded somewhere else. An admin in the app is a lasting admin grant, or a role named by the access rules as `app:role:<name>` that the provider's token carries.
|
|
302
|
+
- A disabled principal, and a person the rules give no tier, are refused with `403`.
|
|
303
|
+
- The tier is read again on every request and on a socket's every frame and once a minute. Losing admission closes the socket with 4403; a change of tier changes what it is sent from then on.
|
|
304
|
+
|
|
305
|
+
### Deny by default
|
|
306
|
+
|
|
307
|
+
One table decides every request before any handler sees it (`src/authz.ts`). A route that is not in it answers `404` to everyone, owners included. Each row says who it answers:
|
|
308
|
+
|
|
309
|
+
| Who | Routes |
|
|
310
|
+
|---|---|
|
|
311
|
+
| Owner | the owners' agent rooms (`api/rooms*`, a `404` to anyone else), the overview, skills, connectors, folders, agents, groups, the archive, prompts, the files pane, and the event stream |
|
|
312
|
+
| Any admitted person | their own conversations, notes and schedules, the host's rooms and pages, uploads, and pictures of agents |
|
|
313
|
+
| Anyone admitted, gated or not | the page itself, `config`, `session`, their notices, push, profile, links and onboarding |
|
|
314
|
+
|
|
315
|
+
A member's socket receives nothing from an agent room, neither as it happens nor when it connects: the owners' cards and the table of who is busy are sent only to sockets whose tier is owner now. A card is answered only by its own audience, the same rule as Discord's: for an approval that names approvers, the named ones who hold its tier now, and the owners; otherwise the turn's speaker and the owners where the scope escalates. A room's readers can never widen that.
|
|
316
|
+
|
|
317
|
+
### What each tier is served
|
|
318
|
+
|
|
319
|
+
`panes` takes a list for the owners and, with `admission: "access"`, `{ owner, admin?, member }` to choose per tier. The default gives a member `rooms` (the host's own rooms only), `conversations`, `notes`, `schedules` and `profile`, of those the host serves; an admin's list is the member's unless given. A member is never served `overview`, `table`, `workspace`, `skills` or `connectors`, and naming one for them stops the start.
|
|
320
|
+
|
|
321
|
+
- **Conversations**: a member lists and reads only the private conversations the registry records as theirs, found by the registry; `exclude(key, record)` is asked on every list and every transcript read.
|
|
322
|
+
- **Notes**: their own only. Asking for another person's is `403`. The pane is left out, with a warning, when the host keeps no memory (`MEMORY`); naming it with none is an error.
|
|
323
|
+
- **Schedules**: their own, whichever of the 0.8 and the current id the schedule names them by; an owner sees every schedule.
|
|
324
|
+
- **Profile**: who they are signed in as, and Log out.
|
|
325
|
+
|
|
326
|
+
### The gate
|
|
327
|
+
|
|
328
|
+
A required step the person still owes (see `onboarding`) closes the app on it: only their profile, settings and the pages that finish a step answer, the page shows the steps with a link to each, and the socket carries only the gate, their notices and refusals. When the last step is done the page loads again with everything the person may now see.
|
|
329
|
+
|
|
330
|
+
### Deep links
|
|
331
|
+
|
|
332
|
+
A link to a page can name where to go after signing in: `?next=profile`, `?next=notices`, `?next=settings`, `?next=room:<key>` for a room of the app's surface, or `?next=p:<page>` for a host page. Anything else, an address of another site included, is dropped.
|
|
333
|
+
|
|
248
334
|
## Install on a device
|
|
249
335
|
|
|
250
336
|
The app is a progressive web app, installable on a phone, tablet or desktop and opened in its own window.
|
|
@@ -289,6 +375,37 @@ Setup:
|
|
|
289
375
|
- **Delivery.** The payload is encrypted for the device (RFC 8291, `aes128gcm`) and signed with a VAPID token (RFC 8292, ES256 with `jose`); there is no push dependency. A `404` or `410` from the service removes the subscription, as do five failures in a row. A person has at most 20 devices.
|
|
290
376
|
- **Who gets it.** Only a device of a person who is still an owner, and only while no open page of that owner reports itself visible.
|
|
291
377
|
|
|
378
|
+
## Notices and the profile
|
|
379
|
+
|
|
380
|
+
A notice is something that waits on one person, such as a customer's message or a card to approve. A host raises one with `Notices.raise({ tag, people, title, open, urgency?, via? })` and takes it back with `Notices.resolve(tag)`; `renotify` adds people to an open one. `open` names exactly where a tap goes: `{ room, message? }` (the room, scrolled to that message) or `{ page, query? }`. A notice appears in the bell, with a count on the app icon where the device allows it, and is pushed according to its `urgency`: a normal one waits in the bell during the person's quiet hours, an urgent one rings and stays on screen until dealt with. `via` narrows a notice to named transports (see `push.transports`).
|
|
381
|
+
|
|
382
|
+
Tapping a notification or a bell row opens exactly that room or page, whether the app is open (its window comes forward and goes there) or closed (a new window opens at the address; a signed-out person signs in and lands there). A device reports when a notice arrived and when it was opened, which is how the app knows a test notification reached the device.
|
|
383
|
+
|
|
384
|
+
The profile page (`profile`) shows the person's name, picture and tier, the accounts they linked and the ones they can link or unlink, **Shown as** (which linked account names them in the app, or Automatic), and their own quiet hours with a test notification. An account that the host's configuration links cannot be unlinked there.
|
|
385
|
+
|
|
386
|
+
### An external push sender
|
|
387
|
+
|
|
388
|
+
With `push.sender: { external: { publicKey } }` the app holds no VAPID key: it queues each push as a row in `web_push_outbox` and a separate process empties it. Run that process with `runPushSender` from `pi-roundtable-web/push-sender`:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import { runPushSender, webPushTransport } from "pi-roundtable-web/push-sender";
|
|
392
|
+
|
|
393
|
+
runPushSender({
|
|
394
|
+
database, // a role that may SELECT the outbox, subscriptions, sessions and principals, UPDATE the outbox, and INSERT deliveries and subscriptions
|
|
395
|
+
transports: [webPushTransport({ /* the VAPID key pair and subject */ }), mySlackTransport],
|
|
396
|
+
render: (row) => ({ title: "Something needs you" }), // with push.content "none" the row carries no text
|
|
397
|
+
subscribe: { // omit when the app's own subscription routes are used
|
|
398
|
+
listener: { port: 8788 },
|
|
399
|
+
path: "/push/subscriptions", // the app's push.sender.external.subscribePath
|
|
400
|
+
origin: "https://app.example.com",
|
|
401
|
+
authenticate: (request) => webSessionPrincipal(database, request),
|
|
402
|
+
},
|
|
403
|
+
logger,
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
The sender tries its transports in order for each row until one says it was sent, narrowed by the row's `via`. Its subscription routes answer only with the app's session cookie, and every request other than a GET must carry exactly the console's `Origin`, or it is refused with `403`. `webSessionPrincipal` and `webSessionClaims` read the session cookie, the second giving the claims of a person's newest live session (set `auth.storeClaims` for them to be stored). Nothing in the app process can read the key.
|
|
408
|
+
|
|
292
409
|
## Authentication
|
|
293
410
|
|
|
294
411
|
The app never makes an account.
|
|
@@ -300,6 +417,7 @@ You choose how with the `auth` option; the console then looks the person up in t
|
|
|
300
417
|
| `{ mode: "verifier", verifier }`, or the `verifier` option as shorthand | The person the verifier reports, linked to an owner's principal. | A proxy such as Cloudflare Access authenticates people in front of the listener. This is the one mode whose proof an agent on the host cannot forge. |
|
|
301
418
|
| `{ mode: "password" }` | The primary owner, once they give the password. | The console is reachable from the internet with no proxy that signs people in. |
|
|
302
419
|
| `{ mode: "none" }` | The primary owner, whoever reaches the listener. | Only the owner can reach the listener: a unix socket, or loopback TCP. |
|
|
420
|
+
| `{ mode: "oidc", issuer, clientId }` | The person the provider reports, by the identity `IDENTITY` links to a principal. With `admission: "access"`, members and admins too. | People sign in with a provider of your own, without a proxy in front. See [OpenID Connect](#openid-connect). |
|
|
303
421
|
|
|
304
422
|
Giving both `auth` and `verifier` throws, and so does giving neither: the plugin never starts without a way to authenticate.
|
|
305
423
|
Whatever the mode, a request that changes data must carry the console's own `Origin`.
|
|
@@ -370,6 +488,18 @@ The console never resolves an identity the way a chat surface does: a visitor is
|
|
|
370
488
|
It refuses an identity of the `legacy` provider too, as resolving refuses it: that is the alias 0.9 keeps for a 0.8 id, not anyone's sign-in.
|
|
371
489
|
A verifier may vouch for members as well as owners, through a proxy that lets the whole company in or an allowlist that grows, so the console never guesses whose an identity is: only a link says so.
|
|
372
490
|
|
|
491
|
+
### OpenID Connect
|
|
492
|
+
|
|
493
|
+
`auth: { mode: "oidc", issuer, clientId }` signs people in with any OpenID Connect provider (Microsoft Entra, Google, Keycloak, Authentik and the like) by the authorization code flow with PKCE. The origin must be `https`: the session cookie is `Secure`.
|
|
494
|
+
|
|
495
|
+
1. A person with no session is sent to the provider with a state, a nonce and a PKCE challenge, kept in a signed cookie that lives ten minutes and works once. The first visit asks silently (`prompt=none`), so a live single sign-on needs no click; when the provider answers `login_required` the person is asked once, normally.
|
|
496
|
+
2. At the callback the code is exchanged, and the `id_token` is checked once: its signature against the provider's published keys, issuer, audience, expiry and nonce. A state that does not match or was used before, a callback with no pending cookie, and a token that fails any check are refused and make no session.
|
|
497
|
+
3. `IDENTITY` admits the person (see [Who is let in](#who-is-let-in)); only then is a session made: a random 256-bit id in an `HttpOnly` cookie, of which the database keeps only the SHA-256, in the `web_sessions` table. No token, claim, code or session id is logged.
|
|
498
|
+
4. The session slides: it ends after `sessionDays` (30) without a visit, and a visit moves the limit at most once an hour. Nothing the provider said is trusted again; the tier is read from `IDENTITY` on every request. Disabling the principal refuses the next request and closes the socket.
|
|
499
|
+
5. Log out deletes the row, so the old cookie is refused afterwards, clears the cookie and, with `endSession`, also signs out at the provider. A person who just logged out is asked, not signed in silently again.
|
|
500
|
+
|
|
501
|
+
`reauthHours` makes the provider vouch again: past it, the next page load does a silent round trip that refreshes the stored facts and roles, a provider that wants the person sends them through a normal sign-in, and a request to the API or the socket on such a session is answered `401` until then. `storeClaims` keeps the named `id_token` claims with the session, replaced at each sign-in, for a sender that needs, say, a mailbox address.
|
|
502
|
+
|
|
373
503
|
### Upgrading from 0.8
|
|
374
504
|
|
|
375
505
|
The core [Migrating to 0.9](../../docs/migration-0.9.md) guide covers backfill, rollback, and the principal isolation boundary.
|
|
@@ -444,7 +574,7 @@ It may be asynchronous; a thrown error or a rejected promise counts as a refusal
|
|
|
444
574
|
|
|
445
575
|
**What the app protects.**
|
|
446
576
|
The conversations hold everything the host's people said to the assistant and what the assistant answered, including tool calls and their results, and the memory notes shape every later turn.
|
|
447
|
-
The app serves that to the host's owners and nobody else, and since 0.23 it also acts for them: a signed-in session can run turns, approve held actions, switch yolo mode on, send files
|
|
577
|
+
The app serves that to the host's owners and nobody else, and since 0.23 it also acts for them: a signed-in session can run turns, approve held actions, switch yolo mode on, send files, change an agent's prompt, model and skills, make, archive and delete agents and groups.
|
|
448
578
|
So the authentication guards the owner's power over the agents, not only their reading.
|
|
449
579
|
Every write, over REST or the WebSocket, needs an authenticated owner whose tier is read again, and an open socket closes (`4401`, `4403`) when the credential lapses or the person stops being an owner.
|
|
450
580
|
Every owner sees every conversation and can read and change every principal's notes; it has no per-user views for members, and no audit trail beyond the host's log, which records only that a request was refused and why.
|
|
@@ -527,7 +657,7 @@ Every option is checked when the plugin is created, so a bad setting stops the h
|
|
|
527
657
|
|
|
528
658
|
| Option | Default | What it does |
|
|
529
659
|
|---|---|---|
|
|
530
|
-
| `auth` | none, `auth` or `verifier` required | How requests are authenticated: `{ mode: "none" }`, `{ mode: "password" }` or `{ mode: "
|
|
660
|
+
| `auth` | none, `auth` or `verifier` required | How requests are authenticated: `{ mode: "none" }`, `{ mode: "password" }`, `{ mode: "verifier", verifier }` or `{ mode: "oidc", ... }`; its keys are in the next table, and [Authentication](#authentication) explains the modes. |
|
|
531
661
|
| `verifier` | none | Shorthand for `auth: { mode: "verifier", verifier }`: decides whether a request comes from someone your proxy authenticated, and reports who. `cloudflareAccess(...)` is built in; pass your own function to use another proxy. Giving both `auth` and `verifier` throws. |
|
|
532
662
|
| `origin` | none, required | The app's own origin as the browser sees it, such as `https://app.example.com`: scheme and host (and port), no path. Requests that change data must carry it as their `Origin`, the dashboard line links to it, and push derives its default contact from it. |
|
|
533
663
|
| `ownerId` | the primary owner | Deprecated. The owner's principal id that a request from a verifier reporting no actor stands for. The Notes pane no longer needs it: it shows each owner their own notes. |
|
|
@@ -535,11 +665,14 @@ Every option is checked when the plugin is created, so a bad setting stops the h
|
|
|
535
665
|
| `mountPath` | `/console` | The path the app is served under: one or more segments of letters, digits, `.`, `_`, `~`, and `-`. |
|
|
536
666
|
| `listener` | `public` | The id of the listener whose address serves the app. |
|
|
537
667
|
| `routing` | `hash` | `path` retains links such as `/console/skills`, with history navigation and an authenticated page fallback. |
|
|
538
|
-
| `
|
|
668
|
+
| `admission` | `owners` | `owners` lets in only people who hold the owner role; `access` lets in everyone `IDENTITY` admits on the surface. See [Members and the gate](#members-and-the-gate). |
|
|
669
|
+
| `modes` | on | `false` leaves out the yolo and long-running switches: none is shown or set, and `PUT api/rooms/<key>/mode` answers 404. |
|
|
670
|
+
| `panes` | `rooms`, `table`, `workspace`, `overview`, `schedules`, `conversations`, `notes` | Ordered subset of those panes plus `skills` and `connectors`. `schedules`, `table` and `workspace` need `rooms`, and `workspace` needs a place to show (below). With `admission: "access"` the default adds `profile`, and `{ owner, admin?, member }` chooses per tier. Disabled panes answer 404. `skills` and `connectors` require their feature ports at setup. An explicit list is kept as written, so a host that listed its panes before 0.23 adds `rooms`, `table` and `schedules` by hand, and one that listed them before 0.24 adds `workspace`. |
|
|
539
671
|
| `workspace` | the agents' directories | What the [files pane](#the-files-pane) shows: `workspace.roots` adds places, each with an `id` (`workspace.roots.id`, a lower-case word other than `agents` and `scratch`), a `label` (`workspace.roots.label`), an absolute `dir` (`workspace.roots.dir`) and `writable` (`workspace.roots.writable`, false unless set), and `workspace.agents` / `workspace.scratch` set to `false` leave the agents' working directory or scratch directory out. A `dir` that is or contains `dataDir` stops the start. |
|
|
540
672
|
| `surface` | `app` | The key prefix of the app's rooms, a lower-case word that is not `discord`. Rooms keep their conversations under it, so changing it starts every room over. |
|
|
541
673
|
| `cardGraceSeconds` | `120` | How long a turn waits on an approval or question card before it goes on without the answer, above 0 and at most 3600. The card stays open for a day. |
|
|
542
674
|
| `uploadMaxBytes` | `104857600` (100 MiB) | The most bytes of one file a person may upload, from 1 to 4 GiB; the agent takes files up to it too. It raises the listener's request body limit. See [Uploads](#uploads). |
|
|
675
|
+
| `uploads` | `{ keepDays: 30 }` | `uploads.keepDays`: how many days a stored file of the app's agent rooms is kept, a whole number from 1 to 3650. A host kind's own `retention.fileDays` decides for its rooms. |
|
|
543
676
|
| `push` | on, with an `https` origin | Web Push; its keys are in the table below, and [Notifications](#notifications) describes the whole. |
|
|
544
677
|
| `icons` | the package's own | `{ dir }`: a directory of the five install icons, see [Install on a device](#install-on-a-device). |
|
|
545
678
|
| `appearance` | `{ look: "modern" }` | `{ look }`: `modern` or `camelot`. The page's default look and the manifest's splash colors; the owner can switch it in the app. |
|
|
@@ -547,19 +680,31 @@ Every option is checked when the plugin is created, so a bad setting stops the h
|
|
|
547
680
|
| `presentation` | English | `{ locale, messages }`: locale controls dates and document language, and `zh-TW` also selects the [built-in Traditional Chinese wording](#presentation-and-built-in-zh-tw); English source strings are dictionary keys for labels, descriptions, confirmations, and API errors. |
|
|
548
681
|
| `features` | none | Trusted host integrations, or a factory receiving public plugin setup context; see [Host feature ports](#host-feature-ports). |
|
|
549
682
|
| `relayNotes` | the remote MCP note | Text that an outside agent's relayed message begins with, taken off before the owner's words are shown. Set it to the same value as the `relayNote` of [pi-roundtable-mcp][mcp] when you changed that one. |
|
|
550
|
-
| `exclude` | none | A function from a channel key to `true` for conversations the console must neither list nor read, such as the channels of another plugin that keeps its own sessions in the same directory. |
|
|
683
|
+
| `exclude` | none | A function from a channel key (and the registry's record of the conversation, when there is one) to `true` for conversations the console must neither list nor read, such as the channels of another plugin that keeps its own sessions in the same directory. |
|
|
551
684
|
| `assetDir` | the package's `dist/` | The built page's directory. Set it only in tests. |
|
|
552
685
|
|
|
553
686
|
The `auth` keys:
|
|
554
687
|
|
|
555
688
|
| Key | Default | What it does |
|
|
556
689
|
|---|---|---|
|
|
557
|
-
| `auth.mode` | none, required | `none`, `password` or `
|
|
690
|
+
| `auth.mode` | none, required | `none`, `password`, `verifier` or `oidc`. |
|
|
558
691
|
| `auth.trustNetwork` | `false` | `none` only: allows a TCP listener that reaches beyond this machine, when a network in front of it keeps everyone else out. |
|
|
559
692
|
| `auth.passwordEnv` | `ROUNDTABLE_WEB_PASSWORD` | `password` only: the environment variable that holds the password, which must be set and at least 12 characters. |
|
|
560
693
|
| `auth.sessionDays` | `30` | `password` only: how long a login lasts, 1 to 365 days. |
|
|
561
694
|
| `auth.principal` | the primary owner | `none` and `password`: the owner's principal id the session speaks for. |
|
|
562
695
|
| `auth.verifier` | none | `verifier` only: the request verifier, as the `verifier` option. |
|
|
696
|
+
| `auth.issuer` | none, required | `oidc` only: the provider's exact `iss`, an `https` URL. |
|
|
697
|
+
| `auth.discoveryUrl` | `<issuer>/.well-known/openid-configuration` | `oidc` only: where the provider's endpoints are read at start; the start fails if they cannot be. |
|
|
698
|
+
| `auth.clientId` | none, required | `oidc` only: the client id registered with the provider; the redirect URI is `<origin><mountPath>/auth/callback`. |
|
|
699
|
+
| `auth.clientSecretEnv` | none | `oidc` only: the environment variable that holds the client secret; set means a confidential client, left out a public client with PKCE. |
|
|
700
|
+
| `auth.scopes` | `openid`, `profile` | `oidc` only. `offline_access` is refused: the app keeps no refresh token. |
|
|
701
|
+
| `auth.subjectClaim` | `sub` | `oidc` only: the claim that names the person (Entra: `oid`). |
|
|
702
|
+
| `auth.nameClaim` | `name`, then `preferred_username` | `oidc` only: the claim for their name. |
|
|
703
|
+
| `auth.rolesClaim` | `roles` | `oidc` only: the claim whose values become `app:role:<name>`; `false` reads none. |
|
|
704
|
+
| `auth.check` | none | `oidc` only: a function from the token's claims to whether the token is acceptable at all. |
|
|
705
|
+
| `auth.endSession` | `false` | `oidc` only: whether Log out also signs out at the provider. |
|
|
706
|
+
| `auth.reauthHours` | never | `oidc` only: ask the provider again after this many hours of one session. |
|
|
707
|
+
| `auth.storeClaims` | none | `oidc` only: `id_token` claims kept with the session. |
|
|
563
708
|
|
|
564
709
|
The `push` keys:
|
|
565
710
|
|
|
@@ -568,7 +713,19 @@ The `push` keys:
|
|
|
568
713
|
| `push.preview` | `true` | Whether a notification shows the first 120 characters of the message. With `false` it says only that something waits, and where. |
|
|
569
714
|
| `push.quietHours` | `{ start: "23:00", end: "08:00" }` | The hours when an agent's post and a notice are not pushed, as two different `HH:MM` times on the console's time zone; an `end` before the `start` crosses midnight. Cards are always pushed. `false` turns quiet hours off. |
|
|
570
715
|
| `push.endpointHosts` | none | Push services to post to besides the browsers' own (Google, Mozilla, Apple, Microsoft), as host names; a leading `*.` allows every subdomain. Endpoints must still be `https`. |
|
|
716
|
+
| `push.tiers` | `["owner"]` | The tiers whose people may subscribe and receive push. |
|
|
571
717
|
| `push.subject` | `origin`, when it is `https` | The contact a push service may use about this server (the VAPID `sub`): a `mailto:` or `https:` URL. With an `http` origin push stays off until it is set. |
|
|
718
|
+
| `push.sender` | `"internal"` | Who signs and sends a push. `"internal"`: this process, with its own VAPID key. `{ external: { publicKey, subscribePath? } }`: a sender process you run holds the key, and this app never does; `publicKey` (base64url) is what browsers subscribe against, and `subscribePath` (starting with `/`) is where the sender serves the subscription routes, when it does. See [An external push sender](#an-external-push-sender). |
|
|
719
|
+
| `push.content` | `"full"` internal, `"none"` external | What leaves the app for a push: `"full"` is a title and a preview, `"none"` is the kind, a tag and a count only (the sender words it). A host room never previews. |
|
|
720
|
+
| `push.transports` | none | Names of the transports besides `web-push` that the sender process carries, such as `slack`. A notice's `via` may name only these and `web-push`; any other name is a `ConfigError` when the notice is raised. The app loads no transport itself. |
|
|
721
|
+
|
|
722
|
+
The `onboarding` keys (what a person owes before the app opens, see [The gate](#the-gate)):
|
|
723
|
+
|
|
724
|
+
| Key | Default | What it does |
|
|
725
|
+
|---|---|---|
|
|
726
|
+
| `onboarding.links` | none | Maps a provider name to `"required"` (gates the app until the account is linked) or `"nudge"` (only asks). Each provider needs a `LinkProver` registered through `WEB_APP`; the person links it on their profile page. A prover's `requires` decides the order. |
|
|
727
|
+
| `onboarding.push` | `"off"` | `"required"` or `"nudge"` asks the person to turn push on for a device and receive a test notification; `"off"` leaves it out. |
|
|
728
|
+
| `onboarding.pushWaiver` | `false` | Lets a person skip a `"required"` push step, which then counts as done and is recorded as waived. |
|
|
572
729
|
|
|
573
730
|
The bad settings that stop the host at startup are: no `auth` or `verifier`, both of them, an unknown `auth.mode`, a password shorter than 12 characters, `none` on a listener that reaches beyond the machine, an `origin` with a path, a `mountPath` of `/` or with `..` in it, an unknown or repeated pane, `schedules`, `table` or `workspace` without `rooms`, a `workspace` pane with no place to show, a `workspace.roots` entry that is malformed, reuses an id, or is or contains `dataDir`, a `surface` that is not a lower-case word or is `discord`, an empty `ownerId`, a `uploadMaxBytes` out of range, and a `push.subject` that is neither a `mailto:` address nor an `https:` URL, and a `push.quietHours` that is neither `false` nor two different `HH:MM` times.
|
|
574
731
|
|
|
@@ -577,7 +734,7 @@ The host refuses to start with a message naming the missing service when one of
|
|
|
577
734
|
|
|
578
735
|
## The console's panes
|
|
579
736
|
|
|
580
|
-
The page has
|
|
737
|
+
The page has ten panes; `panes` chooses which ones a host serves.
|
|
581
738
|
Rooms and Schedules belong to the [owner app](#the-owner-app), and the Round table to [its own section](#the-round-table); the rest are the console that came first, which reads what the host already stores: the conversation files under the data directory, the memory addon, and the agent team.
|
|
582
739
|
|
|
583
740
|
| Pane | What it shows |
|
|
@@ -587,9 +744,10 @@ Rooms and Schedules belong to the [owner app](#the-owner-app), and the Round tab
|
|
|
587
744
|
| Overview | Every active agent with its model, thinking level, state (working where, waiting, or idle), context use, schedule count, and last activity; every group with its members, host, and busy count. An agent links to its Discord channel and its transcript, and a group to its Discord channel. |
|
|
588
745
|
| Conversations | Every conversation stored on disk, in four sections: owner channels (direct messages and channels where the owner talks to the assistant), agent channels, group conversations (each member's conversation inside a group), and outside-agent conversations opened over MCP. Each row shows the channel's name (when the host has a Discord connection), when it was last active, its size, and how many archives it has. An outside-agent conversation shows its first message, when it appears in the first 256 KB of the file, and when it began. A conversation the host's registry records also shows whose it is, by their display name, and whether it is private to them or shared. Every owner sees every conversation. |
|
|
589
746
|
| Notes | A principal's memory in three tabs, core, notes, and events, with search: the signed-in owner's own by default, and any principal's the host knows through a picker that appears when there is more than one. The owner can add a note, edit its text, kind, and date, and delete it after a confirmation, in whichever memory is shown. The memory addon's own validation applies, so a refusal shows its reason and saves nothing. |
|
|
590
|
-
| Skills | The full host catalog, source, groups and carriers; each available skill opens its ordered frontmatter and Markdown body. |
|
|
747
|
+
| Skills | The full host catalog, source, groups and carriers; each available skill opens its ordered frontmatter and Markdown body, and an owner edits an agent-written skill or makes a new one. |
|
|
591
748
|
| Connectors | Upstream gateway enabled/reachable/tool status, virtual-server tool lists, and the agents or profiles that use them; an optional administration link. |
|
|
592
749
|
| Files | The agents' workspace: a tree, highlighted files, an editor with conflict-checked saves, and a restorable trash (see [The files pane](#the-files-pane)). Needs `rooms` and a place to show. |
|
|
750
|
+
| Profile | Who the person is signed in as, their tier, and Log out; it stays open while a step is owed. Part of the [members' app](#members-and-the-gate). |
|
|
593
751
|
| Round table | Every active agent at a podium of one council hall, the coordinator first, each in the state its room is in; a seat turns the camera to its agent and opens a dialogue box on the agent's room (see [The round table](#the-round-table)). Needs `rooms`. |
|
|
594
752
|
|
|
595
753
|
Opening a conversation shows its **transcript**: the user's and the assistant's messages, the tools the assistant called with a short preview of their arguments, and each tool's result (collapsed).
|
|
@@ -664,13 +822,34 @@ webConsole({
|
|
|
664
822
|
|
|
665
823
|
When serving Skills and Connectors, provide those hooks as well; a missing port fails startup rather than silently dropping a pane.
|
|
666
824
|
|
|
825
|
+
## Host rooms, pages and cards
|
|
826
|
+
|
|
827
|
+
A host plugin that is not an agent can have rooms of its own in the app: a support inbox with one room per email thread, say. It lists `WEB_APP` in its `requires` and registers from `setup`:
|
|
828
|
+
|
|
829
|
+
```ts
|
|
830
|
+
const app = context.service(WEB_APP);
|
|
831
|
+
app.registerRoomKind({ kind: "inbox", label: "Support inbox", list, access, header, send, act, buttons, retention });
|
|
832
|
+
app.registerPage({ id: "mailboxes", label: "Mailboxes", place: "settings", visible, view, act });
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
A room's key is `<surface>:<kind>.<id>`, with an id of 8 to 64 letters, digits, `-` or `_`. The kind name is lower case and none of `agent`, `group`, `thread` or `home`. Everything is checked when the web service starts.
|
|
836
|
+
|
|
837
|
+
- **Access is the host's, asked every time.** `access(viewer, id)` answers `{ read, send, act }` for each read, frame, send, card answer, upload and file; `undefined` makes the room, its messages and its files a `404` for that person. `send` is `note` (a message for the room's people), `turn` (a message the room's agent answers), both, or neither; `act` lets them press buttons and run actions. `list(viewer, { filter, cursor })` returns the rooms the person may open, and `undefined` hides the kind. `changed(room)` and `accessChanged(principalId)` on `app.rooms` tell the app a header, a list or a person's access may differ.
|
|
838
|
+
- **Who wrote what.** A host posts with `app.rooms.post(room, { author: { kind, name, label?, principalId?, picture? }, text, files?, reference? })`. The author kinds are `external` (someone outside, shown with the host's label such as "Customer"; plain text with links), `member`, `assistant` (an agent partner; Markdown, raw HTML shown as text) and `notice` (a line of the room's own, such as "Assigned to Mel"; Markdown). `reference` is the host's own id, unique per room, and `byReference` finds it again; `edit` and `remove` change a message. An import may give an earlier `at`, kept within retention.
|
|
839
|
+
- **Cards.** `app.rooms.card(room, { title, body, tone?, state?, links?, buttons? })` puts a card in the room; `updateCard` replaces it by id. A button calls the kind's `act(viewer, id, { action, card }, input)`; a button may carry a `form` (text, textarea, url, select with `initial` and `search`, multiselect), and its input is checked against the form on the server before `act` runs. `act` answers `{ ok: true, notice?, open?, url? }` or `{ ok: false, error, field? }`; `open` is a room (with a message) or a page (with a query). Buttons show only while the card's `state` is `open`, and `buttons(viewer, id, card)` may narrow which ones a person sees. A card's `links` are `https` or a path of this origin. Approval and question cards of the room's agent follow the agent's own prompt scope, never `access` or `buttons`.
|
|
840
|
+
- **Links and pictures.** `app.rooms.link(room, messageId?)` gives the same-origin address that opens a room and highlights a message (`<mount>/room/<key>?message=<id>` with `routing: "path"`, `<mount>/#/room/<key>?message=<id>` with hash routing). `` in a card, a page or an assistant or notice message shows an image the host saved with `saveFile` (rooms) or `app.pages.saveAsset` (pages): PNG, JPEG or WebP whose bytes prove it, served only to those who may see the room or page. Anything else is shown as text.
|
|
841
|
+
- **Pages.** A page is at `<mount>/p/<id>` and is built from sections: Markdown, a list of rows (each may open a link and carry buttons), a form with an action, and a card. `place` is `main` (the navigation), `settings` (the Settings pane) or `hidden`. `visible(viewer)` is asked on every fetch and action; a page that is not visible is a `404`. `app.pages.changed(id?)` sends the viewers who may see it a `page_changed` frame and their page fetches the view again.
|
|
842
|
+
- **Retention.** A kind's `retention: { textDays, fileDays }` deletes its messages, author names, host cards and files that old, counted from each message's `at`. Without it nothing is deleted.
|
|
843
|
+
- **Read marks.** The page tells the app how far a person has read (the newest message in view, at most once a second, never while the page is hidden). The app keeps one cursor per person and room, for host rooms and agent rooms alike (among owners), and computes each room's `unread` from it; the list and the drawer show the count, an attention dot where the host sets `attention`, and the room opens with a "new messages" divider where the person stopped. Under each message the people who have read up to it are named: "Seen by 2", with their pictures; a person's own mark is not shown to them.
|
|
844
|
+
- **The page is lazy.** The room, the lists and the pages load as separate chunks when first opened, so the app's first load is the same size as before.
|
|
845
|
+
|
|
667
846
|
## App protocol
|
|
668
847
|
|
|
669
848
|
The `rooms` pane's page and any client of your own talk to the host in this protocol. A page opens one WebSocket at `<mount>/api/socket` with the subprotocol `roundtable.app.v1`. The upgrade passes the same authenticator as every request and must come from the console's own origin; the socket closes with `4401` when the credential lapses.
|
|
670
849
|
|
|
671
|
-
- Page to server: `hello {visible, focus?}`, `visibility`, `focus {room?}`, `send {id, room, text, uploads?}`, `stop {room}`, `approval {prompt, approved}`, `answer {prompt, answer}`, `answers {prompt, answers, note?}`, `cancel {prompt}`.
|
|
850
|
+
- Page to server: `hello {visible, focus?}`, `visibility`, `focus {room?}`, `send {id, room, text, uploads?}`, `stop {room}`, `approval {prompt, approved}`, `answer {prompt, answer}`, `answers {prompt, answers, note?}`, `cancel {prompt}`; for a host room also `subscribe {room}`, `unsubscribe {room}`, `act {id, room, action, card?, input?}` and `read {room, upTo}`.
|
|
672
851
|
- Server to page: `ready {protocol, principal, name, uploads: {perMessage, maxBytes}}` (then every open card), `accepted`, `refused {id?, prompt?, code}` (`id` names a refused `send`, `prompt` a refused answer to a card), `message`, `progress` (only for the focused room), `activity {room, typing, stoppable, phase?, agent?, outcome?}` (`phase` is `working` during a tool call and `thinking` otherwise; `agent` names the member speaking in a group round; `outcome` is `done` or `failed` and comes only on the frame where `typing` turns false), `presence {agents: {<name>: {busy}}}` (after `ready` and when it changes; `busy` includes work in Discord channels), `edited` (a stored message changed its text, such as a thread's start line once the thread exists), `prompt`, `prompt_closed {prompt, outcome, note?, by?, answer?, closedAt?}`, `mode`, `auto_approved`, `changed`.
|
|
673
|
-
- REST under `<mount>/api/`: `session`, `rooms`, `rooms/<key>/messages?before=&limit=` (`{messages, more, cards}`: the cards of the room that opened within the page's span, from the first message's time on, or from the beginning when `more` is false, and before the first message of the next page), `PUT rooms/<key>/mode`, `PUT rooms/<key>/mute` (body `{muted: boolean}`; the room's push is off for every owner while true; a waiting card still pushes), `POST rooms/<key>/start-over`, `POST uploads`, `DELETE uploads/<id>`, `files/<id>`, `schedules` (`GET`, and `DELETE schedules/<id>`), `agents/<name>` (`GET`, `PATCH`), `PUT agents/<name>/character` (body `{character}`, at most 4 KiB; `400 {error, field}` for a look that is not a character), `models`, `workspace` (the [files pane](#the-files-pane)'s calls), and for push `GET push/key`, `GET`, `POST` and `DELETE push/subscriptions` and `DELETE push/subscriptions/<id>`. The agents' pictures are at `<mount>/avatars/<hash>.png`.
|
|
852
|
+
- REST under `<mount>/api/`: `session`, `rooms`, `rooms/<key>/messages?before=&limit=` (`{messages, more, cards}`: the cards of the room that opened within the page's span, from the first message's time on, or from the beginning when `more` is false, and before the first message of the next page), `PUT rooms/<key>/mode`, `PUT rooms/<key>/mute` (body `{muted: boolean}`; the room's push is off for every owner while true; a waiting card still pushes), `PUT rooms/<key>/pin` (body `{pinned: boolean, before?: key | null}`) and `PUT rooms/<key>/folder` (body `{folder: id | null, before?: key | null}`; `before` is the room it goes before, none or an unknown room for the end; `400` for Home and a dispatch thread, `404` for an unknown room or folder), `POST folders` (body `{name}`, `201`), `PATCH folders/<id>` (body `{name?, before?: id | null}`), `DELETE folders/<id>`, `PUT folders/<id>/mute` (body `{muted: boolean}`; answers `{muted, rooms}`), all of which answer with the whole layout `{pins, folders}` (except the mute), and `GET rooms` answering `{rooms, pins, folders}`, `POST rooms/<key>/start-over`, `POST uploads`, `DELETE uploads/<id>`, `files/<id>`, `schedules` (`GET`, and `DELETE schedules/<id>`), `agents/<name>` (`GET`, `PATCH`), `GET agents/<name>/prompt-history` (`{versions}`, newest first), `GET prompts` (`{documents}`), `GET` and `PUT prompts/<id>` (body `{text, base}`; `409 {error, conflict, current}` when `base` is not the text in force), `POST prompts/<id>/revert` (body `{base}`; the same `409`), `GET prompts/<id>/history`, `GET` and `PUT skill-docs/<name>` (body `{text, baseHash}`; `409` with `current`), `POST skill-docs` (body `{text}`; `201`), `GET skill-docs/<name>/history`, `PUT agents/<name>/character` (body `{character}`, at most 4 KiB; `400 {error, field}` for a look that is not a character), `POST agents` (body `{name, displayName, prompt, model?, thinking?, task?, character?, skills?}`; `201` with the profile; `400` for what the agent server refuses, `{error, field}` for a look), `POST agents/<name>/archive`, `POST agents/<name>/restore` (the profile), `DELETE agents/<name>` (an archived agent only; `400` for an active one), `GET` and `PUT agents/<name>/skills` (`{skills: [{name, description?, source, builtin, carried, missing?}]}`; the body of `PUT` is `{add?, remove?}`, one change that an unknown or built-in name refuses whole; `404` when the host has no skills), `POST groups` (body `{name, displayName, members, host?}`; `201`), `GET`, `PATCH` (`{displayName?, members?, host?}`) and `DELETE groups/<name>`, `POST groups/<name>/archive` and `POST groups/<name>/restore`, `GET archive` (`{entries}`: the archived agents and groups), `models`, `workspace` (the [files pane](#the-files-pane)'s calls), and for push `GET push/key`, `GET`, `POST` and `DELETE push/subscriptions` and `DELETE push/subscriptions/<id>`. The agents' pictures are at `<mount>/avatars/<hash>.png`.
|
|
674
853
|
- Beside the API: `<mount>/manifest.webmanifest`, `<mount>/sw.js`, `<mount>/icons/<name>.png`, `<mount>/theme-init.js`, and in password mode `<mount>/login`, `POST <mount>/auth/login` and `POST <mount>/auth/logout`.
|
|
675
854
|
|
|
676
855
|
A room key from a page must be one of the app's rooms whose agent or group exists; every other key, Discord's included, is refused. Every write needs an owner, whose tier is read again, and REST writes also need the console's `Origin`. A card can be answered by any owner, except a card that is the speaker's alone, which only the speaker's principal answers. After `cardGraceSeconds` (default 120) a turn goes on without the answer and the card stays open for a day; an answer then starts a new turn in the room. The types are in `src/api-types.ts`.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* The account pages (notices, onboarding, profile) and the bell. Loaded with the first of them, not with the first page. */
|
|
2
|
+
.bell {
|
|
3
|
+
position: relative;
|
|
4
|
+
}
|
|
5
|
+
.bell-count {
|
|
6
|
+
position: absolute;
|
|
7
|
+
top: 0.25rem;
|
|
8
|
+
right: 0.15rem;
|
|
9
|
+
min-width: 1.05rem;
|
|
10
|
+
padding: 0 0.28rem;
|
|
11
|
+
border-radius: var(--radius-full);
|
|
12
|
+
background: var(--warn);
|
|
13
|
+
color: var(--bg);
|
|
14
|
+
font-size: 0.65rem;
|
|
15
|
+
font-weight: var(--weight-bold);
|
|
16
|
+
line-height: 1.05rem;
|
|
17
|
+
text-align: center;
|
|
18
|
+
}
|
|
19
|
+
.notices {
|
|
20
|
+
list-style: none;
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
.notice[data-urgent] {
|
|
25
|
+
border-color: var(--warn);
|
|
26
|
+
}
|
|
27
|
+
.steps {
|
|
28
|
+
margin: 0;
|
|
29
|
+
padding: 0;
|
|
30
|
+
list-style: none;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: var(--space-3);
|
|
34
|
+
}
|
|
35
|
+
.steps > li {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: flex-start;
|
|
39
|
+
gap: var(--space-2);
|
|
40
|
+
padding-left: var(--space-3);
|
|
41
|
+
border-left: 3px solid var(--border);
|
|
42
|
+
}
|
|
43
|
+
.steps > li[data-done] {
|
|
44
|
+
border-left-color: var(--accent);
|
|
45
|
+
}
|
|
46
|
+
.who {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: var(--space-3);
|
|
50
|
+
}
|
|
51
|
+
.who-picture {
|
|
52
|
+
width: 3.5rem;
|
|
53
|
+
height: 3.5rem;
|
|
54
|
+
border-radius: var(--radius-full);
|
|
55
|
+
object-fit: cover;
|
|
56
|
+
background: var(--surface-2);
|
|
57
|
+
}
|
|
58
|
+
.who-initial {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
width: 3.5rem;
|
|
63
|
+
height: 3.5rem;
|
|
64
|
+
border-radius: var(--radius-full);
|
|
65
|
+
background: var(--accent-soft);
|
|
66
|
+
color: var(--accent);
|
|
67
|
+
font-size: 1.4rem;
|
|
68
|
+
font-weight: var(--weight-bold);
|
|
69
|
+
}
|
|
70
|
+
.link-code {
|
|
71
|
+
display: block;
|
|
72
|
+
width: fit-content;
|
|
73
|
+
font-family: var(--mono, ui-monospace, monospace);
|
|
74
|
+
font-size: 1.6rem;
|
|
75
|
+
letter-spacing: 0.15em;
|
|
76
|
+
padding: var(--space-2) var(--space-3);
|
|
77
|
+
border: 1px dashed var(--border);
|
|
78
|
+
border-radius: var(--radius);
|
|
79
|
+
user-select: all;
|
|
80
|
+
}
|
|
81
|
+
.choice {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
gap: var(--space-2);
|
|
85
|
+
border: 0;
|
|
86
|
+
padding: 0;
|
|
87
|
+
margin: 0;
|
|
88
|
+
}
|
|
89
|
+
.choice label {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: row;
|
|
92
|
+
justify-content: flex-start;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--space-2);
|
|
95
|
+
min-height: var(--touch);
|
|
96
|
+
}
|
|
97
|
+
.quiet-row {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-wrap: wrap;
|
|
100
|
+
gap: var(--space-3);
|
|
101
|
+
align-items: center;
|
|
102
|
+
}
|