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.
Files changed (223) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +195 -16
  3. package/dist/account-zk4efb87.css +102 -0
  4. package/dist/chunk-account-hw49gd0d.js +1 -0
  5. package/dist/chunk-action-menu-rm5cyqkj.js +1 -0
  6. package/dist/chunk-agent-b017vswv.js +1 -0
  7. package/dist/chunk-arranged-rooms-q5v3dm1r.js +1 -0
  8. package/dist/chunk-character-creator-5dxcvd16.js +1 -0
  9. package/dist/chunk-codeeditor-pyt09fwg.js +2 -0
  10. package/dist/chunk-confirm-521yqmta.js +1 -0
  11. package/dist/chunk-connectors-hrm06nx8.js +1 -0
  12. package/dist/chunk-conversations-e26rrf01.js +1 -0
  13. package/dist/chunk-drag-v41gx2mc.js +1 -0
  14. package/dist/chunk-entry-jxxb3ath.js +1 -0
  15. package/dist/chunk-folder-list-b97xk12b.js +1 -0
  16. package/dist/chunk-folds-b8q7eags.js +1 -0
  17. package/dist/chunk-gate-8k61stsc.js +1 -0
  18. package/dist/chunk-group-scene-dx6wt19m.js +1 -0
  19. package/dist/chunk-group-table-e4jr8r3a.js +1 -0
  20. package/dist/chunk-host-state-g7e7c0k5.js +1 -0
  21. package/dist/chunk-host-ui-x7k6n07v.js +939 -0
  22. package/dist/chunk-index-014rjv9k.js +2 -0
  23. package/dist/chunk-index-2d0kg096.js +2 -0
  24. package/dist/chunk-index-2qd81wbx.js +2 -0
  25. package/dist/chunk-index-2stynxw6.css +1 -0
  26. package/dist/chunk-index-459drsft.js +2 -0
  27. package/dist/chunk-index-4mfj596y.js +2 -0
  28. package/dist/chunk-index-66nskrdj.js +47 -0
  29. package/dist/chunk-index-awx6nnta.js +2 -0
  30. package/dist/chunk-index-bbsytycb.js +2 -0
  31. package/dist/chunk-index-c07p8k0x.js +4 -0
  32. package/dist/chunk-index-cq1e9xdz.js +2 -0
  33. package/dist/chunk-index-d1prgfbp.js +2 -0
  34. package/dist/chunk-index-dhpt8qr9.js +2 -0
  35. package/dist/chunk-index-e5sd30xy.js +2 -0
  36. package/dist/chunk-index-g351921r.js +1 -0
  37. package/dist/chunk-index-hby0ap91.js +2 -0
  38. package/dist/chunk-index-hssjbr1k.js +2 -0
  39. package/dist/chunk-index-k2qq1gdj.js +2 -0
  40. package/dist/chunk-index-ke9ynm0n.js +2 -0
  41. package/dist/chunk-index-km40bhsz.js +2 -0
  42. package/dist/chunk-index-kpscvg85.js +2 -0
  43. package/dist/chunk-index-m0d100mg.js +2 -0
  44. package/dist/chunk-index-mb72snk9.js +2 -0
  45. package/dist/chunk-index-mjqqver5.js +4 -0
  46. package/dist/chunk-index-nqfkwkfp.js +2 -0
  47. package/dist/chunk-index-nwqk447w.js +2 -0
  48. package/dist/chunk-index-pcfhpm8v.js +302 -0
  49. package/dist/chunk-index-q8h8xzbm.js +2 -0
  50. package/dist/chunk-index-r5vszgfs.js +2 -0
  51. package/dist/{chunk-index-mpm90318.js → chunk-index-rhtvcbsq.js} +2 -2
  52. package/dist/chunk-index-rjv7vrwd.js +13 -0
  53. package/dist/chunk-index-v23m38p6.js +50 -0
  54. package/dist/chunk-index-va0g7d88.js +1 -0
  55. package/dist/{chunk-index-6t68gbex.js → chunk-index-wnjp6wv0.js} +2 -2
  56. package/dist/chunk-index-xgkd15n3.js +2 -0
  57. package/dist/chunk-index-y8rnq8zf.js +14 -0
  58. package/dist/chunk-index-z3k18atc.js +2 -0
  59. package/dist/chunk-lang-css-6bepqpnx.js +1 -0
  60. package/dist/{chunk-lang-dockerfile-ey0nvhhm.js → chunk-lang-dockerfile-hrav55t7.js} +1 -1
  61. package/dist/{chunk-lang-go-403qsw9f.js → chunk-lang-go-x9ahv6e5.js} +3 -3
  62. package/dist/chunk-lang-html-8jc72zt4.js +1 -0
  63. package/dist/chunk-lang-javascript-2p2qtbnf.js +1 -0
  64. package/dist/chunk-lang-json-hsemyd0r.js +1 -0
  65. package/dist/chunk-lang-markdown-zkwb4c9n.js +6 -0
  66. package/dist/{chunk-lang-python-9jk5a49y.js → chunk-lang-python-tewx7006.js} +2 -2
  67. package/dist/chunk-lang-rust-0ydx84s1.js +1 -0
  68. package/dist/{chunk-lang-shell-cgq6q0kj.js → chunk-lang-shell-gq1tmdz4.js} +1 -1
  69. package/dist/chunk-lang-sql-xt1qyycg.js +2 -0
  70. package/dist/{chunk-lang-toml-xh45nm68.js → chunk-lang-toml-0hamc5fc.js} +1 -1
  71. package/dist/chunk-lang-typescript-82xf7z2e.js +1 -0
  72. package/dist/{chunk-lang-xml-1mh879j9.js → chunk-lang-xml-wf6h5pt2.js} +1 -1
  73. package/dist/{chunk-lang-yaml-jac98srt.js → chunk-lang-yaml-7vk521h1.js} +1 -1
  74. package/dist/chunk-layout-actions-zh7bezk7.js +1 -0
  75. package/dist/chunk-menu-items-1n9qargb.js +2 -0
  76. package/dist/chunk-notes-t15bh04m.js +1 -0
  77. package/dist/chunk-notice-bell-w3y0h0sr.js +1 -0
  78. package/dist/chunk-notices-fa3g7qze.js +1 -0
  79. package/dist/chunk-onboarding-34v3nr17.js +1 -0
  80. package/dist/chunk-overview-km5vq375.js +1 -0
  81. package/dist/chunk-palette-a8txdgd0.js +1 -0
  82. package/dist/chunk-placement-items-wgaxsbvs.js +1 -0
  83. package/dist/chunk-profile-ypxdbsk1.js +1 -0
  84. package/dist/chunk-prompt-history-wm9dsy6s.js +1 -0
  85. package/dist/chunk-prompts-7mhhq7zs.js +1 -0
  86. package/dist/chunk-room-confirms-t11a1gsw.js +1 -0
  87. package/dist/chunk-room-menu-2tm2xtfx.js +1 -0
  88. package/dist/chunk-rooms-menu-button-wxnsxjwc.js +1 -0
  89. package/dist/chunk-schedules-2ft6xxyw.js +1 -0
  90. package/dist/chunk-seat-menu-d3npdtx1.js +1 -0
  91. package/dist/chunk-sheet-hosts-vngvp7wv.js +1 -0
  92. package/dist/chunk-skill-editor-65n2hsq4.js +8 -0
  93. package/dist/chunk-skills-shkdwes8.js +1 -0
  94. package/dist/chunk-switcher-list-g6hhmr1t.js +1 -0
  95. package/dist/chunk-table-1b6bsq2y.js +1 -0
  96. package/dist/chunk-table-seats-vy1qhdks.js +1 -0
  97. package/dist/chunk-table2d-n26b7gd0.js +1 -0
  98. package/dist/chunk-table3d-8aysyf56.js +4220 -0
  99. package/dist/chunk-transcript-355faceq.js +1 -0
  100. package/dist/chunk-where-j5qdt8a4.js +1 -0
  101. package/dist/chunk-workspace-vrbs6zx1.js +1 -0
  102. package/dist/chunk-zh-tw-zeg600bk.js +1 -0
  103. package/dist/index.html +1 -1
  104. package/dist/sw.js +1 -1
  105. package/package.json +5 -4
  106. package/src/api-types.ts +515 -6
  107. package/src/app/api-http.ts +66 -0
  108. package/src/app/app.ts +214 -33
  109. package/src/app/card-log.ts +6 -0
  110. package/src/app/desk.ts +47 -22
  111. package/src/app/files.ts +73 -7
  112. package/src/app/layout-routes.ts +239 -0
  113. package/src/app/layout.ts +133 -0
  114. package/src/app/presence.ts +2 -2
  115. package/src/app/prompt-routes.ts +167 -0
  116. package/src/app/protocol.ts +63 -1
  117. package/src/app/room-log.ts +148 -14
  118. package/src/app/rooms.ts +23 -0
  119. package/src/app/routes.ts +278 -156
  120. package/src/app/skill-doc-routes.ts +122 -0
  121. package/src/app/socket.ts +309 -77
  122. package/src/app/sockets.ts +108 -20
  123. package/src/app/surface.ts +72 -36
  124. package/src/app/team-routes.ts +457 -0
  125. package/src/app/threads.ts +14 -0
  126. package/src/app/uploads.ts +33 -7
  127. package/src/auth/authenticator.ts +71 -8
  128. package/src/auth/next.ts +52 -0
  129. package/src/auth/oidc-pages.ts +60 -0
  130. package/src/auth/oidc.ts +611 -0
  131. package/src/auth/sessions.ts +193 -0
  132. package/src/authz.ts +172 -0
  133. package/src/console-api.ts +122 -25
  134. package/src/console-server.ts +70 -6
  135. package/src/gate.ts +90 -0
  136. package/src/host/access.ts +161 -0
  137. package/src/host/cards.ts +99 -0
  138. package/src/host/context.ts +141 -0
  139. package/src/host/forms.ts +237 -0
  140. package/src/host/frames.ts +342 -0
  141. package/src/host/host-app.ts +176 -0
  142. package/src/host/host-context.ts +49 -0
  143. package/src/host/host-routes.ts +363 -0
  144. package/src/host/pages.ts +266 -0
  145. package/src/host/reads.ts +155 -0
  146. package/src/host/retention.ts +41 -0
  147. package/src/host/room-views.ts +137 -0
  148. package/src/host/rooms-service.ts +414 -0
  149. package/src/host/stored-json.ts +13 -0
  150. package/src/host/views.ts +103 -0
  151. package/src/i18n/zh-tw.ts +309 -10
  152. package/src/layout-order.ts +210 -0
  153. package/src/notices/api.ts +43 -0
  154. package/src/notices/mount.ts +292 -0
  155. package/src/notices/service.ts +294 -0
  156. package/src/notices/store.ts +298 -0
  157. package/src/notices/urls.ts +38 -0
  158. package/src/onboarding/api.ts +32 -0
  159. package/src/onboarding/service.ts +149 -0
  160. package/src/onboarding/steps.ts +75 -0
  161. package/src/onboarding/waivers.ts +40 -0
  162. package/src/options.ts +498 -34
  163. package/src/profile/api.ts +175 -0
  164. package/src/profile/links.ts +162 -0
  165. package/src/push/api.ts +189 -52
  166. package/src/push/dispatch.ts +156 -0
  167. package/src/push/drain.ts +139 -0
  168. package/src/push/mutes.ts +17 -0
  169. package/src/push/outbox.ts +218 -0
  170. package/src/push/prefs.ts +118 -0
  171. package/src/push/sender.ts +7 -2
  172. package/src/push/triggers.ts +56 -5
  173. package/src/push/vapid.ts +19 -0
  174. package/src/push/web-push-transport.ts +106 -0
  175. package/src/push-sender/index.ts +14 -0
  176. package/src/push-sender/run.ts +118 -0
  177. package/src/push-sender/session.ts +54 -0
  178. package/src/visitors.ts +94 -6
  179. package/src/web-app.ts +674 -0
  180. package/src/web-plugin.ts +293 -26
  181. package/dist/chunk-action-menu-7v5fce2v.js +0 -1
  182. package/dist/chunk-agent-2rw4ydw2.js +0 -1
  183. package/dist/chunk-agent-sheet-2q24dkkf.js +0 -3
  184. package/dist/chunk-character-creator-8q6ympxj.js +0 -1
  185. package/dist/chunk-codeeditor-fgmq298t.js +0 -2
  186. package/dist/chunk-connectors-mr53r7fh.js +0 -1
  187. package/dist/chunk-conversations-4vy2z6w9.js +0 -1
  188. package/dist/chunk-entry-jg1eem82.js +0 -1
  189. package/dist/chunk-group-scene-na95b1te.js +0 -1
  190. package/dist/chunk-index-11rjg8a8.js +0 -1
  191. package/dist/chunk-index-1y50d6sj.js +0 -2
  192. package/dist/chunk-index-5y2r0eaj.js +0 -47
  193. package/dist/chunk-index-7fphxxzd.js +0 -14
  194. package/dist/chunk-index-80f1zkp6.css +0 -1
  195. package/dist/chunk-index-87h45frg.js +0 -2
  196. package/dist/chunk-index-bnwmssyn.js +0 -2
  197. package/dist/chunk-index-ckgbszwt.js +0 -51
  198. package/dist/chunk-index-fmjjekhd.js +0 -2
  199. package/dist/chunk-index-t1m65yn6.js +0 -13
  200. package/dist/chunk-index-t711t17m.js +0 -2
  201. package/dist/chunk-index-txv7qvyy.js +0 -2
  202. package/dist/chunk-index-wr3dg24d.js +0 -2
  203. package/dist/chunk-index-yx683d5n.js +0 -2
  204. package/dist/chunk-lang-css-fkdg3x8x.js +0 -1
  205. package/dist/chunk-lang-html-v8h9g977.js +0 -1
  206. package/dist/chunk-lang-javascript-ykb2vhaa.js +0 -1
  207. package/dist/chunk-lang-json-2rze3rpy.js +0 -1
  208. package/dist/chunk-lang-markdown-6w2htnfc.js +0 -6
  209. package/dist/chunk-lang-rust-24fggcqx.js +0 -1
  210. package/dist/chunk-lang-sql-gjywb9ke.js +0 -2
  211. package/dist/chunk-lang-typescript-qh6rebyb.js +0 -1
  212. package/dist/chunk-menu-items-zjpyj51b.js +0 -2
  213. package/dist/chunk-notes-sjba7kw3.js +0 -1
  214. package/dist/chunk-overview-26r77kb6.js +0 -1
  215. package/dist/chunk-room-confirms-851184ep.js +0 -1
  216. package/dist/chunk-room-menu-8y9c026y.js +0 -1
  217. package/dist/chunk-skills-56s4hdbn.js +0 -1
  218. package/dist/chunk-table-qykpvp32.js +0 -1
  219. package/dist/chunk-table2d-k60ressj.js +0 -1
  220. package/dist/chunk-table3d-bv2ajk39.js +0 -4220
  221. package/dist/chunk-transcript-vekz1mh6.js +0 -1
  222. package/dist/chunk-workspace-hevefd0d.js +0 -1
  223. package/dist/chunk-zh-tw-5r2wjre0.js +0 -1
package/src/web-plugin.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { join } from "node:path";
2
3
  import {
3
4
  AGENTS,
@@ -6,10 +7,14 @@ import {
6
7
  IDENTITY,
7
8
  MEMORY,
8
9
  MODES,
10
+ PROFILES,
11
+ PROMPTS,
9
12
  type RoundtablePlugin,
10
13
  SCHEDULES,
14
+ SKILLS,
11
15
  } from "pi-roundtable";
12
16
  import { DISCORD } from "pi-roundtable/discord";
17
+ import type { PaneName } from "./api-types.ts";
13
18
  import { APP_MIGRATIONS, OwnerApp } from "./app/app.ts";
14
19
  import { APP_PROTOCOL } from "./app/protocol.ts";
15
20
  import { UPLOAD_FRAMING_BYTES } from "./app/uploads.ts";
@@ -19,8 +24,16 @@ import {
19
24
  createAuthenticator,
20
25
  WEB_CONSOLE_IDENTITY,
21
26
  } from "./auth/authenticator.ts";
22
- import { ConsoleApi } from "./console-api.ts";
27
+ import { discoverProvider, OidcLogin } from "./auth/oidc.ts";
28
+ import { loadSessionKey } from "./auth/session-key.ts";
29
+ import { WebSessions } from "./auth/sessions.ts";
30
+ import {
31
+ type AccessView,
32
+ ConsoleApi,
33
+ type ConsolePorts,
34
+ } from "./console-api.ts";
23
35
  import { ConsoleServer } from "./console-server.ts";
36
+ import { Gate } from "./gate.ts";
24
37
  import {
25
38
  protectedDirs,
26
39
  type ResolvedOptions,
@@ -30,7 +43,11 @@ import {
30
43
  workspaceRootsOf,
31
44
  } from "./options.ts";
32
45
  import { loadVapid } from "./push/vapid.ts";
33
- import { consoleVisitors } from "./visitors.ts";
46
+ import { accessVisitors, consoleVisitors } from "./visitors.ts";
47
+ import { viewerOf, WEB_APP, WebAppRegistry } from "./web-app.ts";
48
+
49
+ /** How often sessions past their limit are deleted: daily. */
50
+ const SESSION_SWEEP_MS = 86_400_000;
34
51
 
35
52
  /** The longest a WebSocket stays open under `password` auth: the session cookie is looked at again after this. */
36
53
  const PASSWORD_SOCKET_LIFE_MS = 3_600_000;
@@ -47,11 +64,16 @@ export function webConsole(options: WebConsoleOptions): RoundtablePlugin {
47
64
  const { auth } = settings;
48
65
  const rooms = settings.panes.includes("rooms");
49
66
  const schedules = settings.panes.includes("schedules");
67
+ const migrations = [
68
+ ...(rooms ? APP_MIGRATIONS : []),
69
+ ...(auth.mode === "oidc" ? [WebSessions.migration] : []),
70
+ ];
50
71
  return definePlugin({
51
72
  name: "web-console",
52
- ...(rooms ? { migrations: APP_MIGRATIONS } : {}),
53
- // `none` and `password` speak for one principal through this identity; a verifier brings its own.
54
- ...(auth.mode === "verifier"
73
+ ...(migrations.length > 0 ? { migrations } : {}),
74
+ provides: [WEB_APP],
75
+ // `none` and `password` speak for one principal through this identity; a verifier or a sign-in brings its own.
76
+ ...(auth.mode !== "none" && auth.mode !== "password"
55
77
  ? {}
56
78
  : {
57
79
  identities: [
@@ -66,7 +88,10 @@ export function webConsole(options: WebConsoleOptions): RoundtablePlugin {
66
88
  ...(settings.panes.includes("overview") || rooms ? [AGENTS] : []),
67
89
  ...(rooms ? [MODES] : []),
68
90
  ...(schedules ? [SCHEDULES] : []),
69
- ...(settings.panes.includes("notes") ? [MEMORY] : []),
91
+ // A list that names the notes pane needs the memory; the default list drops it without one.
92
+ ...(settings.panesGiven && settings.panes.includes("notes")
93
+ ? [MEMORY]
94
+ : []),
70
95
  ],
71
96
  setup: (context) => build(settings, context),
72
97
  });
@@ -76,6 +101,20 @@ type Context = Parameters<RoundtablePlugin["setup"]>[0];
76
101
 
77
102
  async function build(settings: ResolvedOptions, context: Context) {
78
103
  const { services, queue, logger, env } = context;
104
+ // What the other plugins register their rooms, pages and steps in: sealed when the web service starts.
105
+ const webApp = new WebAppRegistry(settings.surface);
106
+ services.provide(WEB_APP, webApp);
107
+ const external = settings.push.sender !== "internal";
108
+ const vapidFile = join(settings.dataDir, "web", "vapid.json");
109
+ // An external sender holds the key: the app never makes or reads one, and a leftover file would say it does.
110
+ if (external && existsSync(vapidFile))
111
+ throw new Error(
112
+ `web-console: push.sender is external, but ${vapidFile} exists. The app must not hold a push key: delete the file (devices subscribe again against the sender's key)`,
113
+ );
114
+ if (settings.admission === "access" && env.agentTools === "full")
115
+ logger.warn(
116
+ "web-console: admission is access and the agents carry shell tools. Anyone who can write the database can make a web session, and an agent with a shell can; keep admission owners behind a signing proxy, or run the agent server with tools chat-only",
117
+ );
79
118
  const features =
80
119
  typeof settings.features === "function"
81
120
  ? await settings.features(context)
@@ -108,9 +147,20 @@ async function build(settings: ResolvedOptions, context: Context) {
108
147
  "web-console: the workspace pane needs a directory to show: the agents' workspace (the agent server provides it) or workspace.roots",
109
148
  );
110
149
  }
111
- const panes = settings.panes.filter(
112
- (pane) => pane !== "workspace" || workspaceRoots.length > 0,
113
- );
150
+ // The default list shows the notes pane only when the host keeps memory.
151
+ const memory = services.find(MEMORY);
152
+ const withoutNotes = !settings.panesGiven && memory === undefined;
153
+ if (withoutNotes && settings.panes.includes("notes"))
154
+ logger.warn("the notes pane is left out: this host keeps no memory");
155
+ const servable = (pane: PaneName) =>
156
+ (pane !== "workspace" || workspaceRoots.length > 0) &&
157
+ (pane !== "notes" || !withoutNotes);
158
+ const panes = settings.panes.filter(servable);
159
+ const tierPanes = {
160
+ owner: panes,
161
+ admin: settings.tierPanes.admin.filter(servable),
162
+ member: settings.tierPanes.member.filter(servable),
163
+ };
114
164
  if (workspaceRoots.length > 0) checkRoots(workspaceRoots, guarded);
115
165
  const workspace =
116
166
  panes.includes("workspace") && rooms
@@ -124,27 +174,106 @@ async function build(settings: ResolvedOptions, context: Context) {
124
174
  // Absent on a host without the registry: the console then lists conversations found by name only.
125
175
  const registry = services.find(CONVERSATIONS);
126
176
  const identity = services.get(IDENTITY);
177
+ const profiles = services.find(PROFILES);
178
+ if (settings.admission === "access" && !profiles)
179
+ throw new Error(
180
+ "web-console: admission access needs the PROFILES service; a plugin that replaces IDENTITY must provide PROFILES too",
181
+ );
182
+ // An agent's skills are chosen from what the Skills pane lists when the host supplies it, else from the addon's own catalog.
183
+ const skillRegistry = services.find(SKILLS);
184
+ const skills = skillRegistry
185
+ ? {
186
+ catalog: () =>
187
+ features?.skills
188
+ ? features.skills.catalog()
189
+ : skillRegistry.catalog(),
190
+ attach: (
191
+ agent: string,
192
+ add: readonly string[],
193
+ remove: readonly string[],
194
+ ) => skillRegistry.attach(agent, add, remove),
195
+ documents: skillRegistry,
196
+ }
197
+ : undefined;
198
+ // The prompts plugin is always on; a host that replaced it may leave it out.
199
+ const prompts = services.find(PROMPTS);
127
200
  const listeners: (() => void)[] = [];
128
201
  const changed = () => {
129
202
  for (const listener of listeners) listener();
130
203
  };
131
204
  // A broken page stops startup here, before anything connects.
132
205
  const assets = loadAssets(settings.assetDir);
206
+ const gate = new Gate({ registry: webApp });
207
+ const hosted: ConsolePorts["hosted"] = async (visitor) => {
208
+ const viewer = viewerOf(visitor);
209
+ const pages: {
210
+ id: string;
211
+ label: string;
212
+ place: "main" | "settings" | "hidden";
213
+ }[] = [];
214
+ for (const page of webApp.hostPages()) {
215
+ try {
216
+ if (await page.visible(viewer))
217
+ pages.push({
218
+ id: page.id,
219
+ label: page.label,
220
+ place: page.place,
221
+ });
222
+ } catch (error) {
223
+ logger.warn(
224
+ { page: page.id, err: error },
225
+ "a page's visibility was not read",
226
+ );
227
+ }
228
+ }
229
+ const kinds: { kind: string; label: string }[] = [];
230
+ for (const kind of webApp.kinds()) {
231
+ try {
232
+ if ((await kind.list(viewer, {})) !== undefined)
233
+ kinds.push({ kind: kind.kind, label: kind.label });
234
+ } catch (error) {
235
+ logger.warn(
236
+ { kind: kind.kind, err: error },
237
+ "a kind of room was not listed",
238
+ );
239
+ }
240
+ }
241
+ return { pages, kinds };
242
+ };
243
+ const access: AccessView | undefined =
244
+ settings.admission === "access"
245
+ ? {
246
+ push: {
247
+ tiers: settings.push.tiers,
248
+ ...(settings.push.sender !== "internal"
249
+ ? {
250
+ key: settings.push.sender.external.publicKey,
251
+ ...(settings.push.sender.external.subscribePath
252
+ ? {
253
+ subscribe: settings.push.sender.external.subscribePath,
254
+ }
255
+ : {}),
256
+ }
257
+ : {}),
258
+ },
259
+ gate: (visitor) => gate.required(viewerOf(visitor)),
260
+ }
261
+ : undefined;
133
262
  const api = new ConsoleApi({
134
263
  title: settings.title,
135
264
  timeZone: env.timeZone,
136
265
  panes,
266
+ tierPanes,
267
+ ...(access ? { access } : {}),
268
+ hosted,
137
269
  sessionsDir: settings.sessionsDir,
138
270
  ...(team ? { team } : {}),
139
271
  queue,
140
272
  ...(connection
141
273
  ? { channelName: (id: string) => connection.channelInfo(id) }
142
274
  : {}),
143
- ...(settings.panes.includes("notes")
144
- ? {
145
- memoryOf: (principalId: string) =>
146
- services.get(MEMORY).forSpeaker(principalId),
147
- }
275
+ ...(panes.includes("notes") && memory
276
+ ? { memoryOf: (principalId: string) => memory.forSpeaker(principalId) }
148
277
  : {}),
149
278
  people: identity,
150
279
  ...(settings.exclude ? { exclude: settings.exclude } : {}),
@@ -157,16 +286,45 @@ async function build(settings: ResolvedOptions, context: Context) {
157
286
  characterSkins: env.characterSkins,
158
287
  ...(settings.routing === "path" ? { mountPath: settings.mount } : {}),
159
288
  ...(rooms
160
- ? { app: { surface: settings.surface, protocol: APP_PROTOCOL } }
289
+ ? {
290
+ app: {
291
+ surface: settings.surface,
292
+ protocol: APP_PROTOCOL,
293
+ ...(settings.modes ? {} : { modes: false as const }),
294
+ },
295
+ }
161
296
  : {}),
162
297
  ...(settings.presentation ? { presentation: settings.presentation } : {}),
163
298
  });
299
+ const identify =
300
+ settings.admission === "access"
301
+ ? accessVisitors(identity, { surface: settings.surface, logger })
302
+ : consoleVisitors(identity, {
303
+ logger,
304
+ ...(settings.ownerId ? { ownerId: settings.ownerId } : {}),
305
+ });
306
+ // An open socket closes when its session ends; the app is made after the sign-in it hangs on.
307
+ let sessionEnded: (id: string) => void = () => undefined;
308
+ let sweeper: ReturnType<typeof setInterval> | undefined;
309
+ const sessions =
310
+ settings.auth.mode === "oidc"
311
+ ? new WebSessions(context.database(), { days: settings.auth.sessionDays })
312
+ : undefined;
313
+ const oidc =
314
+ settings.auth.mode === "oidc" && sessions
315
+ ? await startOidc(settings.auth, {
316
+ sessions,
317
+ settings,
318
+ identify,
319
+ logger,
320
+ events: context.events,
321
+ ended: (id) => sessionEnded(id),
322
+ })
323
+ : undefined;
164
324
  const authenticator = createAuthenticator({
165
325
  auth: settings.auth,
166
- identify: consoleVisitors(identity, {
167
- logger,
168
- ...(settings.ownerId ? { ownerId: settings.ownerId } : {}),
169
- }),
326
+ identify,
327
+ ...(oidc && sessions ? { oidc: { login: oidc, sessions } } : {}),
170
328
  origin: settings.origin,
171
329
  mount: settings.mount,
172
330
  title: settings.title,
@@ -178,18 +336,25 @@ async function build(settings: ResolvedOptions, context: Context) {
178
336
  address: context.listener(settings.listener),
179
337
  },
180
338
  });
181
- if (rooms && !settings.push.subject)
339
+ if (rooms && !external && !settings.push.subject)
182
340
  logger.warn(
183
341
  "web push is off: the origin is not https, so set push.subject to a mailto: or https: contact",
184
342
  );
185
- const vapid = rooms
186
- ? await loadVapid(join(settings.dataDir, "web", "vapid.json"))
187
- : undefined;
343
+ const vapid = rooms && !external ? await loadVapid(vapidFile) : undefined;
188
344
  const app =
189
- rooms && vapid
345
+ rooms && (vapid || external)
190
346
  ? new OwnerApp({
347
+ ...(settings.presentation
348
+ ? { presentation: settings.presentation }
349
+ : {}),
350
+ extras: {
351
+ ...(profiles ? { profiles } : {}),
352
+ routing: settings.routing,
353
+ onboarding: settings.onboarding,
354
+ push: settings.push,
355
+ },
191
356
  push: {
192
- vapid,
357
+ ...(vapid ? { vapid } : {}),
193
358
  subject: settings.push.subject,
194
359
  preview: settings.push.preview,
195
360
  ...(settings.push.quietHours
@@ -209,14 +374,29 @@ async function build(settings: ResolvedOptions, context: Context) {
209
374
  dataDir: settings.dataDir,
210
375
  cardGraceSeconds: settings.cardGraceSeconds,
211
376
  uploadMaxBytes: settings.uploadMaxBytes,
377
+ uploadKeepDays: settings.uploadKeepDays,
378
+ events: context.events,
379
+ // The name the person goes by in the app, as the profiles name them.
380
+ ...(profiles
381
+ ? { nameOf: async (id: string) => (await profiles.of(id)).name }
382
+ : {}),
383
+ roomUrl: (room, message) =>
384
+ `${settings.routing === "path" ? `${settings.mount}/room/` : `${settings.mount}/#/room/`}${encodeURIComponent(room)}${message === undefined ? "" : `?message=${message}`}`,
212
385
  authenticator,
213
386
  authMode: settings.auth.mode,
387
+ admission: settings.admission,
388
+ registry: webApp,
389
+ gate,
390
+ pushTiers: settings.push.tiers,
391
+ modeSwitches: settings.modes,
214
392
  // A password session outlives a logout or a new password on an open socket, so the socket is capped.
215
393
  ...(settings.auth.mode === "password"
216
394
  ? { socketLifeMs: PASSWORD_SOCKET_LIFE_MS }
217
395
  : {}),
218
396
  database: context.database(),
219
397
  agents: services.get(AGENTS),
398
+ ...(skills ? { skills } : {}),
399
+ ...(prompts ? { prompts } : {}),
220
400
  ...(workspace ? { workspace } : {}),
221
401
  modes: services.get(MODES),
222
402
  identity,
@@ -228,7 +408,10 @@ async function build(settings: ResolvedOptions, context: Context) {
228
408
  logger,
229
409
  })
230
410
  : undefined;
231
- if (app) listeners.push(() => app.changed());
411
+ if (app) {
412
+ listeners.push(() => app.changed());
413
+ sessionEnded = (id) => app.sessionEnded(id);
414
+ }
232
415
  const server = new ConsoleServer({
233
416
  mount: settings.mount,
234
417
  assets,
@@ -240,6 +423,8 @@ async function build(settings: ResolvedOptions, context: Context) {
240
423
  ...(settings.presentation ? { presentation: settings.presentation } : {}),
241
424
  api,
242
425
  ...(app ? { app } : {}),
426
+ gate,
427
+ surface: settings.surface,
243
428
  subscribe: (listener) => {
244
429
  listeners.push(listener);
245
430
  queue.onChange(listener);
@@ -252,10 +437,26 @@ async function build(settings: ResolvedOptions, context: Context) {
252
437
  {
253
438
  name: "web-console",
254
439
  start: () => {
440
+ checkRegistered(settings, webApp);
441
+ webApp.seal();
255
442
  server.start();
256
443
  app?.start();
444
+ if (sessions) {
445
+ const sweep = () =>
446
+ sessions
447
+ .sweep()
448
+ .catch((error: unknown) =>
449
+ logger.warn(
450
+ { err: error },
451
+ "old web sessions were not deleted",
452
+ ),
453
+ );
454
+ sweep();
455
+ sweeper = setInterval(sweep, SESSION_SWEEP_MS);
456
+ }
257
457
  },
258
458
  stop: () => {
459
+ clearInterval(sweeper);
259
460
  app?.stop();
260
461
  server.stop();
261
462
  },
@@ -272,3 +473,69 @@ async function build(settings: ResolvedOptions, context: Context) {
272
473
  dashboard: [`Web console: ${settings.origin}${settings.mount}/`],
273
474
  };
274
475
  }
476
+
477
+ /** What the host registered, checked when the service starts: every provider onboarding names has its prover. */
478
+ function checkRegistered(settings: ResolvedOptions, webApp: WebAppRegistry) {
479
+ if (
480
+ settings.onboarding.push !== "off" &&
481
+ (!settings.panes.includes("rooms") ||
482
+ (settings.push.sender === "internal" && !settings.push.subject))
483
+ )
484
+ throw new Error(
485
+ "web-console: onboarding.push asks people to turn push on, but push is off here. It needs the rooms pane and a push.subject (or an external sender)",
486
+ );
487
+ for (const provider of Object.keys(settings.onboarding.links))
488
+ if (!webApp.prover(provider))
489
+ throw new Error(
490
+ `web-console: onboarding.links names ${provider}, but no plugin registered a link prover for it (WEB_APP.registerLinkProver)`,
491
+ );
492
+ }
493
+
494
+ interface OidcParts {
495
+ sessions: WebSessions;
496
+ settings: ResolvedOptions;
497
+ identify: Parameters<typeof createAuthenticator>[0]["identify"];
498
+ logger: Context["logger"];
499
+ events: Context["events"];
500
+ ended(id: string): void;
501
+ }
502
+
503
+ /** Reads the provider's discovery document, the client secret and the session key, and makes the sign-in flow. */
504
+ async function startOidc(
505
+ auth: Extract<ResolvedOptions["auth"], { mode: "oidc" }>,
506
+ parts: OidcParts,
507
+ ): Promise<OidcLogin> {
508
+ const { settings, logger } = parts;
509
+ let clientSecret: string | undefined;
510
+ if (auth.clientSecretEnv) {
511
+ clientSecret = process.env[auth.clientSecretEnv];
512
+ if (!clientSecret)
513
+ throw new Error(
514
+ `web-console: auth.clientSecretEnv names ${auth.clientSecretEnv}, which is not set`,
515
+ );
516
+ }
517
+ return new OidcLogin({
518
+ settings: auth,
519
+ metadata: await discoverProvider(auth),
520
+ ...(clientSecret ? { clientSecret } : {}),
521
+ sessions: parts.sessions,
522
+ key: loadSessionKey(settings.dataDir, logger),
523
+ origin: settings.origin,
524
+ mount: settings.mount,
525
+ surface: settings.surface,
526
+ routing: settings.routing,
527
+ title: settings.title,
528
+ presentation: settings.presentation,
529
+ identify: parts.identify,
530
+ onSignIn: (info) =>
531
+ parts.events.action?.({
532
+ kind: "sign-in",
533
+ actor: info.speakerId,
534
+ principalId: info.principalId,
535
+ ok: true,
536
+ at: new Date(),
537
+ }),
538
+ onEnded: parts.ended,
539
+ logger,
540
+ });
541
+ }
@@ -1 +0,0 @@
1
- import{o,yt,c,r,m,e,i,l,W,pt,En}from"./chunk-index-ckgbszwt.js";var k=()=>typeof matchMedia==="function"&&matchMedia("(max-width: 719px)").matches,x={rtMenu:!0},g=()=>Boolean(window.history.state?.rtMenu),E=8,y=(n)=>n?.isConnected?n:null;function b(n){return n.items.map((t)=>i("button",{type:"button",role:t.checked===void 0?"menuitem":"menuitemcheckbox",...t.checked===void 0?{}:{"aria-checked":t.checked},className:"menu-item menu-link","data-tone":t.tone,onClick:()=>n.onChoose(t),children:[t.icon?e("span",{className:"menu-icon","data-tone":t.tone,children:e(l,{name:t.icon,size:18})}):null,e("span",{className:"menu-text",children:e("strong",{children:t.label})}),t.checked?e(l,{name:"check",size:18,className:"menu-check"}):null]},t.id))}function H({menu:n}){let t=c(void 0);if(n)t.current=n;let u=t.current,f=c(!1),v=c(void 0),p=n?.serial;return o(()=>{if(p===void 0)return;return window.history.pushState(x,"",window.location.href),f.current=!0,()=>{if(f.current)f.current=!1,window.history.back()}},[p]),o(()=>{let h=()=>{if(!f.current||g())return;f.current=!1,pt();let M=v.current;v.current=void 0,M?.()};return window.addEventListener("popstate",h),()=>window.removeEventListener("popstate",h)},[]),e(W,{open:n!==void 0,title:u?.title??"",onClose:pt,restoreFocus:()=>y(u?.source??null),children:u?e("div",{className:"menu action-items",role:"menu","aria-label":u.title,children:e(b,{items:u.items,onChoose:(h)=>{if(!f.current){pt(),h.run();return}v.current=h.run,window.history.back()}})}):null})}function T({menu:n}){let t=c(null),[u,f]=r(n.at),{source:v}=n,p=(s)=>{y(v)?.focus(),pt(),s?.()},h=c(p);h.current=p,yt(()=>{let s=t.current;if(!s)return;let a=s.getBoundingClientRect();f({x:Math.max(E,Math.min(n.at.x,window.innerWidth-a.width-E)),y:Math.max(E,Math.min(n.at.y,window.innerHeight-a.height-E))}),s.querySelector('[role^="menuitem"]')?.focus()},[n.serial]),o(()=>{let s=(d)=>{if(d.target instanceof Node&&t.current?.contains(d.target))return;pt()},a=()=>pt();return document.addEventListener("pointerdown",s,!0),document.addEventListener("wheel",s,{capture:!0,passive:!0}),document.addEventListener("touchmove",s,{capture:!0,passive:!0}),window.addEventListener("resize",a),()=>{document.removeEventListener("pointerdown",s,!0),document.removeEventListener("wheel",s,!0),document.removeEventListener("touchmove",s,!0),window.removeEventListener("resize",a)}},[]);let M=(s)=>{let a=[...t.current?.querySelectorAll('[role^="menuitem"]')??[]],d=a.indexOf(document.activeElement),w=(L)=>{s.preventDefault(),a[(L+a.length)%a.length]?.focus()};switch(s.key){case"Escape":s.preventDefault(),s.stopPropagation(),h.current();return;case"ArrowDown":return w(d+1);case"ArrowUp":return w(d<0?-1:d-1);case"Tab":return w(s.shiftKey?d<0?-1:d-1:d+1);case"Home":return w(0);case"End":return w(-1)}};return e("div",{ref:t,className:"action-popover",role:"menu","aria-label":n.title,tabIndex:-1,style:{left:`${u.x}px`,top:`${u.y}px`},onKeyDown:M,children:e(b,{items:n.items,onChoose:(s)=>p(s.run)})})}function I(){let n=En(),t=c(void 0);if(n&&t.current?.serial!==n.serial)t.current={serial:n.serial,sheet:k()};let u=Boolean(n&&t.current?.sheet);return i(m,{children:[e(H,{menu:u?n:void 0}),n&&!u?e(T,{menu:n},n.serial):null]})}export{I as ActionMenuHost};
@@ -1 +0,0 @@
1
- import{o,e,re,f,P,Re}from"./chunk-index-ckgbszwt.js";function g({name:r}){let m=f((i)=>i.listed),t=f((i)=>Object.values(i.rooms).find((n)=>n.view?.kind==="agent"&&n.view.name===r)?.view?.key,r);return o(()=>{if(t)re("room",t,{replace:!0}),Re(r);else if(m)re("rooms",void 0,{replace:!0})},[t,m,r]),e("div",{className:"page",children:e(P,{})})}export{g as AgentPage};
@@ -1,3 +0,0 @@
1
- import{Q,Ge,ie,o,Oe,q,c,r,t,s,e,i,l,f,ae,W,P,S,y,d,b,vt,on,Cn,An}from"./chunk-index-ckgbszwt.js";import{lt}from"./chunk-index-mpm90318.js";import{st}from"./chunk-index-87h45frg.js";function U(n,m){let a=n.split(`
2
- `),k=m.split(`
3
- `),h=0;while(h<a.length&&h<k.length&&a[h]===k[h])h++;let g=0;while(g<a.length-h&&g<k.length-h&&a[a.length-1-g]===k[k.length-1-g])g++;let N=a.slice(h,a.length-g),w=k.slice(h,k.length-g),u=a.slice(0,h).map((v)=>({kind:"same",text:v}));return u.push(...me(N,w)),u.push(...a.slice(a.length-g).map((v)=>({kind:"same",text:v}))),u}function me(n,m){let a=(v)=>v.map((L)=>({kind:"del",text:L})),k=(v)=>v.map((L)=>({kind:"add",text:L}));if(n.length===0)return k(m);if(m.length===0)return a(n);if(n.length*m.length>4000000)return[...a(n),...k(m)];let h=m.length+1,g=new Uint32Array((n.length+1)*h);for(let v=n.length-1;v>=0;v--)for(let L=m.length-1;L>=0;L--)g[v*h+L]=n[v]===m[L]?(g[(v+1)*h+L+1]??0)+1:Math.max(g[(v+1)*h+L]??0,g[v*h+L+1]??0);let N=[],w=0,u=0;while(w<n.length&&u<m.length)if(n[w]===m[u])N.push({kind:"same",text:n[w]}),w++,u++;else if((g[(w+1)*h+u]??0)>=(g[w*h+u+1]??0))N.push({kind:"del",text:n[w]}),w++;else N.push({kind:"add",text:m[u]}),u++;return N.push(...a(n.slice(w)),...k(m.slice(u))),N}function X(n){return{added:n.filter((m)=>m.kind==="add").length,removed:n.filter((m)=>m.kind==="del").length}}var ce=ie(()=>(Q("8q6ympxj"),import("./chunk-character-creator-8q6ympxj.js")).then((n)=>({default:n.CharacterCreator}))),fe=["default","off","minimal","low","medium","high","xhigh"],ee={default:s("Follow the assistant (automatic)"),off:s("Off"),minimal:s("Minimal"),low:s("Low"),medium:s("Medium"),high:s("High"),xhigh:s("Very high")},C="default",E=(n)=>t(ee[n]??n),J=(n)=>({displayName:n.displayName,prompt:n.prompt,model:n.model??C,thinking:n.thinking??C});function Y(n,m){return{...m.displayName!==n.displayName?{displayName:m.displayName}:{},...m.prompt!==n.prompt?{prompt:m.prompt}:{},...m.model!==(n.model??C)?{model:m.model}:{},...m.thinking!==(n.thinking??C)?{thinking:m.thinking}:{}}}function Ee(){let n=An(),m=c(n);if(n)m.current=n;let a=m.current,k=n!==void 0;if(o(()=>{if(!k)return;let h=window.location.href,g=()=>{if(window.location.href!==h)on()};return window.addEventListener("popstate",g),window.addEventListener("hashchange",g),()=>{window.removeEventListener("popstate",g),window.removeEventListener("hashchange",g)}},[k]),!a)return null;return e(W,{open:n!==void 0,title:t("Edit agent"),size:"wide",onClose:on,children:e(ue,{name:a.name,opened:a.id},a.name)})}function ue({name:n,opened:m}){let[a,k]=r(),[h,g]=r([]),[N,w]=r(),[u,v]=r(),[L,R]=r(!1),[te,x]=r(!1),[ne,F]=r(!1),se=vt(a?.room??""),M=c(null),T=a!==void 0,V=f((p)=>a?p.rooms[a.room]?.view?.muted===!0:!1,a?.room??""),[oe,H]=r(!1),O=Oe(),B=Oe(),K=Oe();o(()=>{if(!T||!Cn()?.look)return;M.current?.scrollIntoView({block:"start"}),M.current?.focus({preventScroll:!0})},[T,m]);let _=c({});_.current={profile:a,draft:u},o(()=>{let p=!0;return Promise.all([d.agent(n),d.models()]).then(([D,re])=>{if(!p)return;let{profile:G,draft:I}=_.current,de=G&&I&&Object.keys(Y(G,I)).length>0;k(D),g(re.models),v(de?I:J(D))},(D)=>p&&w(b(D))),()=>{p=!1}},[n,m]);let A=q(()=>a&&u?Y(a,u):{},[a,u]);if(N)return e(S,{message:N});if(!a||!u)return e(P,{});let j=Object.keys(A).length>0,le=Array.from(new Set([C,...h,u.model])),z=async()=>{R(!0);try{let p=await d.updateAgent(n,A);k(p),v(J(p)),x(!1),y(t("Saved."))}catch(p){y(b(p),"danger")}finally{R(!1)}};return i("div",{className:"stack agent-sheet",children:[i("header",{className:"profile-head",children:[e(ae,{spec:a.character,name:a.name,size:72,state:se,label:a.displayName}),i("div",{children:[e("h3",{children:a.displayName}),e("p",{className:"meta",children:t("Runs on {model}, thinking {thinking}",{model:a.effective.model,thinking:E(a.effective.thinking)})})]})]}),e("ul",{className:"menu",children:e("li",{children:i("label",{className:"menu-item menu-toggle",htmlFor:K,children:[e("span",{className:"menu-icon",children:e(l,{name:V?"bell-off":"bell"})}),i("span",{className:"menu-text",children:[e("strong",{children:t("Notifications")}),e("span",{className:"meta",children:t("Push this room's posts to every owner's devices. A card that waits for you is always pushed.")})]}),e(lt,{id:K,checked:!V,label:t("Notifications"),busy:oe,onChange:async(p)=>{H(!0),await st(a.room,!p),H(!1)}})]})})}),te?e(ge,{profile:a,draft:u,saving:L,onBack:()=>x(!1),onSave:z}):i("form",{className:"stack form",onSubmit:(p)=>{if(p.preventDefault(),!j)return;if(A.prompt!==void 0)x(!0);else z()},children:[i("label",{htmlFor:B,children:[t("Name"),e("input",{id:B,value:u.displayName,onChange:(p)=>v({...u,displayName:p.target.value})})]}),i("div",{className:"field",children:[i("div",{className:"field-head",children:[e("label",{htmlFor:O,children:t("Prompt")}),i("button",{type:"button",className:"secondary compact","aria-haspopup":"dialog",onClick:()=>F(!0),children:[e(pe,{}),t("Full screen")]})]}),e("textarea",{id:O,rows:8,value:u.prompt,onChange:(p)=>v({...u,prompt:p.target.value})})]}),e(Z,{label:t("Model"),value:u.model,onChange:(p)=>v({...u,model:p}),options:le.map((p)=>({value:p,label:p===C?t("Follow the assistant"):p}))}),e(Z,{label:t("Thinking level"),value:u.thinking,onChange:(p)=>v({...u,thinking:p}),options:fe.map((p)=>({value:p,label:t(ee[p])}))}),e("div",{className:"actions",children:e("button",{type:"submit",disabled:!j||L,children:L?t("Saving…"):A.prompt!==void 0?t("Review changes"):t("Save")})})]}),i("section",{className:"card stack",ref:M,tabIndex:-1,children:[e("h3",{children:t("Look")}),e("p",{className:"meta",children:t("Class, colors and parts. The picture here, in the rooms and in Discord all follow it.")}),e(Ge,{fallback:e(P,{}),children:e(ce,{name:a.name,displayName:a.displayName,prompt:a.prompt,character:a.character,onSaved:(p)=>k(p)})})]}),e(W,{open:ne,title:t("Prompt"),size:"full",onClose:()=>F(!1),children:e("textarea",{className:"prompt-full","aria-label":t("Prompt"),value:u.prompt,onChange:(p)=>v({...u,prompt:p.target.value})})})]})}var pe=()=>e("svg",{viewBox:"0 0 24 24",width:16,height:16,fill:"none",stroke:"currentColor",strokeWidth:1.8,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",children:e("path",{d:"M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"})});function Z(n){return i("fieldset",{className:"field",children:[e("legend",{children:n.label}),e("div",{className:"option-chips",children:n.options.map((m)=>e("button",{type:"button","aria-pressed":m.value===n.value,className:"option-chip",onClick:()=>n.onChange(m.value),children:m.label},m.value))})]})}var he={same:" ",add:"+",del:"−"};function ge(n){let{profile:m,draft:a}=n,k=c(null);o(()=>k.current?.focus(),[]);let h=q(()=>U(m.prompt,a.prompt),[m.prompt,a.prompt]),g=X(h),N=[];if(a.displayName!==m.displayName)N.push({label:t("Name"),before:m.displayName,after:a.displayName});let w=(u)=>u===C?t("Follow the assistant"):u;if(a.model!==(m.model??C))N.push({label:t("Model"),before:w(m.model??C),after:w(a.model)});if(a.thinking!==(m.thinking??C))N.push({label:t("Thinking level"),before:E(m.thinking??C),after:E(a.thinking)});return i("section",{ref:k,className:"stack review",tabIndex:-1,"aria-label":t("Review changes"),children:[e("h3",{children:t("Review changes")}),N.length?e("ul",{className:"review-fields",children:N.map((u)=>i("li",{children:[e("strong",{children:u.label}),e("span",{className:"review-before",children:u.before}),e("span",{"aria-hidden":"true",children:"→"}),e("span",{className:"review-after",children:u.after})]},u.label))}):null,i("div",{className:"field",children:[i("span",{className:"field-label",children:[t("Prompt")," ·"," ",t("Lines: {added} added, {removed} removed",{added:g.added,removed:g.removed})]}),e("pre",{className:"diff",children:h.map((u,v)=>i("span",{className:"diff-line","data-kind":u.kind,children:[e("span",{className:"diff-mark","aria-hidden":"true",children:he[u.kind]}),u.kind==="add"||u.kind==="del"?i("span",{className:"visually-hidden",children:[u.kind==="add"?t("Added"):t("Removed"),":"," "]}):null,u.text||" "]},v))})]}),i("div",{className:"actions",children:[e("button",{type:"button",className:"secondary",disabled:n.saving,onClick:n.onBack,children:t("Back to editing")}),e("button",{type:"button",disabled:n.saving,onClick:n.onSave,children:n.saving?t("Saving…"):t("Save")})]})]})}export{Ee as AgentSheetHost};
@@ -1 +0,0 @@
1
- import{_e,rt,X,Lt,jt}from"./chunk-index-5y2r0eaj.js";import{ce,an,o,Oe,q,c,r,t,s,m,e,i,Ct,bt,ae,W,y,ht,d,b,Pe,Me,ge}from"./chunk-index-ckgbszwt.js";var le={"hat.none":"No hat","hat.goggcap":"Leather cap","hat.tallcap":"Burgundian felt cap","hat.softcap":"Soft cap","hat.hardhat":"Iron skullcap","hat.fcap":"Hunter's cap","hat.winghelm":"Winged helm","hat.helm":"Helm","hat.mortar":"Biretta","hat.beret":"Chaperon","hat.goggles":"Pointed cap","hat.tricorn":"Pilgrim hat","hat.sailor":"Straw hat","hat.kettle":"Kettle hat","hat.cavalier":"Feathered hat","hat.toque":"Linen cook's cap","hair.none":"No hair","beard.none":"No beard","accessory.none":"No accessory","prop.beads":"Prayer beads","prop.marotte":"Marotte","prop.map":"Chart","prop.flask":"Alchemist's phial","prop.telescope":"Astrolabe","prop.wrench":"Dividers","outfit.surplice":"Surplice","outfit.overalls":"Work jerkin","outfit.armor":"Mail hauberk","outfit.apron":"Smith's apron","outfit.chef":"Cook's tunic","outfit.coat":"Doublet","outfit.vest":"Pourpoint","outfit.sailor":"Mariner's tunic","hair.bob":"Bob","hair.bun":"Bun","hair.ponytail":"Ponytail"},U=(a)=>a.charAt(0).toUpperCase()+a.slice(1);function N(a,l){let h=`character.${a}.${l}`,p=t(h);if(p!==h)return p;if(a==="class")return X.classes[l]?.label??U(l);return le[`${a}.${l}`]??U(l)}function V(a){let l=`character.jobs.${a}`,h=t(l);return h!==l?h:X.classes[a]?.jobs??""}var H=[{field:"hat",title:s("Hat"),ids:X.hats},{field:"outfit",title:s("Outfit"),ids:X.outfits},{field:"prop",title:s("Prop"),ids:X.props},{field:"hair",title:s("Hair"),ids:X.hair},{field:"beard",title:s("Beard"),ids:["none",...X.beards]},{field:"accessory",title:s("Accessory"),ids:["none",...X.accessories]}],de=new Set(["class","colors","palette","build","skin","hairColor",...H.map((a)=>a.field)]),Z=Object.keys(X.palettes),x={idle:s("Idle"),thinking:s("Thinking"),working:s("Working"),waiting:s("Waiting for you"),done:s("Done"),error:s("Error"),sleepy:s("Sleepy")},ue={slim:s("Slim"),regular:s("Regular"),stout:s("Stout")};function ee(a){return{class:a.class,outfit:a.outfit,hat:a.hat,prop:a.prop,hair:a.hair,beard:a.beard,accessory:a.accessory,build:a.build,skin:a.skin,hairColor:a.hairColor,colors:{...a.colors}}}var Q=(a)=>JSON.stringify(ee(a)),v=(a)=>a[Math.floor(Math.random()*a.length)];function fe(){return{class:v(_e),outfit:v(X.outfits),hat:v(X.hats),prop:v(X.props),hair:v(X.hair),beard:v(["none",...X.beards]),accessory:v(["none",...X.accessories]),build:v(["slim","regular","stout"]),skin:v([0,1,2,3,4,5]),hairColor:v(X.hairColors),palette:v(Z)}}var te=ce(function(l){return i("button",{type:"button",className:"creator-tile","aria-pressed":l.pressed,title:l.hint?`${l.label} — ${l.hint}`:l.label,onClick:l.onPick,...l.data,children:[e(ae,{spec:l.spec,size:l.size,live:!1}),e("span",{children:l.label})]})});function me(a,l,h){let p=JSON.stringify({...a,[l]:0});return q(()=>h.map((u)=>({...a,[l]:u})),[p,l,h])}function he(){let a=c(null),[l,h]=r();return o(()=>{let p=a.current;if(!p)return;let u=()=>{let S=p.scrollLeft>1,P=p.scrollLeft+p.clientWidth<p.scrollWidth-1;h(S&&P?"both":P?"end":S?"start":void 0)};u(),p.addEventListener("scroll",u,{passive:!0});let g=typeof ResizeObserver>"u"?void 0:new ResizeObserver(u);return g?.observe(p),()=>{p.removeEventListener("scroll",u),g?.disconnect()}},[]),[a,l]}function pe(a){let{part:l}=a,h=me(a.shown,l.field,l.ids),[p,u]=he();return e("div",{className:"creator-strip",ref:p,"data-more":u,children:l.ids.map((g,S)=>e(te,{data:{[`data-${l.field}`]:g},spec:h[S],size:44,label:N(l.field,g),pressed:a.draft[l.field]===g,onPick:()=>a.onPick(g)},g))})}function B(a){return e("button",{type:"button",className:"creator-swatch","aria-pressed":a.pressed,"aria-label":a.label,title:a.label,style:{background:a.background},onClick:a.onPick,...a.data})}function L(a){return i("div",{className:"creator-field","data-field":a.field,children:[a.title?e("h3",{children:a.title}):null,a.children,a.error?e("p",{className:"field-error",role:"alert",children:a.error}):null]})}function He(a){let[l,h]=r(a.character),p=an(l),[u,g]=r({}),[S,P]=r(!1),[T,re]=r("idle"),[K,E]=r(),[R,M]=r(),O=Pe("(max-width: 719px)"),z=c(null),I=Oe(),ie=Me(l),ne=bt(),w=Q(l)!==Q(a.character);o(()=>h(a.character),[a.character]),o(()=>{if(!w)return;let n=(k)=>k.preventDefault(),f=(k)=>{if(k.button!==0||k.metaKey||k.ctrlKey||k.shiftKey||k.altKey)return;let A=k.target instanceof Element?k.target.closest("a"):null;if(!A||A.target||A.hasAttribute("download"))return;if(z.current?.contains(A))return;k.preventDefault(),k.stopPropagation(),E({kind:"link",anchor:A})},F=Ct((k)=>E({kind:"navigate",proceed:k}));return window.addEventListener("beforeunload",n),document.addEventListener("click",f,!0),()=>{F(),window.removeEventListener("beforeunload",n),document.removeEventListener("click",f,!0)}},[w]);let C=(n)=>{g({}),h((f)=>Lt(n,f))},oe=async()=>{P(!0),g({});try{let n=await d.setCharacter(a.name,ee(l));a.onSaved(n),y(t("Saved."))}catch(n){let f=n instanceof ht&&n.field?n.field.split(".")[0]:void 0,F=f&&de.has(f)?f:"*";g({[F]:b(n)})}finally{P(!1)}},se=async()=>{let n=K;if(h(a.character),g({}),n?.kind==="link")setTimeout(()=>n.anchor.click(),0);else if(n?.kind==="navigate")n.proceed()},_=e(L,{field:"class",title:t("Class"),error:u.class,children:e("div",{className:"creator-grid",children:_e.map((n)=>e(be,{id:n,base:p,pressed:l.class===n,onPick:()=>C({class:n,colors:l.colors})},n))})}),j=i(L,{field:"colors",title:t("Colors"),error:u.colors??u.palette,children:[i("fieldset",{className:"creator-swatches",children:[e("legend",{className:"visually-hidden",children:t("Palette")}),Z.map((n)=>{let f=X.palettes[n];return e(B,{data:{"data-palette":n},label:N("palette",n),pressed:l.palette===n,background:`linear-gradient(135deg, ${f.primary} 52%, ${f.trim} 52%)`,onPick:()=>C({palette:n})},n)})]}),e("div",{className:"creator-custom",children:["primary","secondary","trim"].map((n)=>i("label",{children:[e("span",{children:n==="primary"?t("Primary (the accent)"):n==="secondary"?t("Secondary"):t("Trim")}),e("input",{type:"color",value:l.colors[n],onChange:(f)=>C({colors:{[n]:f.target.value}})})]},n))})]}),G=H.map((n)=>e(L,{field:n.field,title:t(n.title),error:u[n.field],children:e(pe,{part:n,draft:l,shown:p,onPick:(f)=>C({[n.field]:f})})},n.field)),J=i(m,{children:[e(L,{field:"build",title:t("Build"),error:u.build,children:i("fieldset",{className:"segmented",children:[e("legend",{className:"visually-hidden",children:t("Build")}),["slim","regular","stout"].map((n)=>i("label",{"data-checked":l.build===n,children:[e("input",{type:"radio",name:`${I}-build`,checked:l.build===n,onChange:()=>C({build:n})}),Y(n)]},n))]})}),e(L,{field:"skin",title:t("Skin tone"),error:u.skin,children:i("fieldset",{className:"creator-swatches",children:[e("legend",{className:"visually-hidden",children:t("Skin tone")}),(ne??X.skins).map((n,f)=>e(B,{data:{"data-skin":String(f)},label:t("Skin tone {n}",{n:f+1}),pressed:l.skin===f,background:n,onPick:()=>C({skin:f})},f))]})}),e(L,{field:"hairColor",title:t("Hair color"),error:u.hairColor,children:i("fieldset",{className:"creator-swatches",children:[e("legend",{className:"visually-hidden",children:t("Hair color")}),X.hairColors.map((n,f)=>e(B,{data:{"data-hair-color":n},label:t("Hair color {n}",{n:f+1}),pressed:l.hairColor===n,background:n,onPick:()=>C({hairColor:n})},n)),e("input",{type:"color","aria-label":t("Hair color"),value:l.hairColor,onChange:(n)=>C({hairColor:n.target.value})})]})})]}),D=[{id:"class",title:t("Class"),summary:N("class",l.class),error:u.class,node:_},{id:"colors",title:t("Colors"),summary:l.palette?N("palette",l.palette):l.colors.primary,error:u.colors??u.palette,node:j},...H.map((n,f)=>({id:n.field,title:t(n.title),summary:N(n.field,l[n.field]),error:u[n.field],node:G[f]})),{id:"body",title:t("Body"),summary:Y(l.build),error:u.build??u.skin??u.hairColor,node:J}];return i("form",{ref:z,className:"creator",style:ie,"aria-labelledby":I,onSubmit:(n)=>{if(n.preventDefault(),w&&!S)oe()},children:[e("h2",{id:I,className:"visually-hidden",children:t("Character of {name}",{name:a.displayName})}),i("div",{className:"creator-preview",children:[e(ae,{spec:l,name:a.name,state:T,size:176,label:t("{name}, {state}",{name:a.displayName,state:t(x[T])})}),e("button",{type:"button",className:"secondary","data-action":"cycle",onClick:()=>re((n)=>rt[(rt.indexOf(n)+1)%rt.length]??"idle"),children:t("Preview states")}),e("span",{className:"meta","aria-live":"polite",children:t(x[T])})]}),i("div",{className:"creator-body",children:[u["*"]?e("p",{className:"field-error",role:"alert","data-field":"*",children:u["*"]}):null,O?e("ul",{className:"creator-sections",children:D.map((n)=>e("li",{children:i("button",{type:"button",className:"creator-section secondary",onClick:()=>M(n.id),children:[e("strong",{children:n.title}),e("span",{className:"meta",children:n.summary}),n.error?e("span",{className:"field-error",role:"alert",children:n.error}):null]})},n.id))}):i(m,{children:[_,j,G,J]}),i("div",{className:"actions creator-actions",children:[e("button",{type:"button",className:"secondary","data-action":"suggest",onClick:()=>{g({}),h(jt({name:a.name,displayName:a.displayName,prompt:a.prompt}))},children:t("Suggest from role")}),e("button",{type:"button",className:"secondary","data-action":"randomize",onClick:()=>C(fe()),children:t("Randomize")}),e("button",{type:"button",className:"secondary","data-action":"cancel",onClick:()=>{if(w)E({kind:"cancel"})},children:t("Cancel")}),e("button",{type:"submit","data-action":"save",disabled:!w||S,children:S?t("Saving…"):t("Save")})]})]}),e(W,{open:O&&R!==void 0,title:D.find((n)=>n.id===R)?.title??"",onClose:()=>M(void 0),children:e("div",{className:"stack",children:D.find((n)=>n.id===R)?.node})}),e(ge,{open:K!==void 0,title:t("Discard your changes?"),confirmLabel:t("Discard"),cancelLabel:t("Keep editing"),tone:"danger",onClose:()=>E(void 0),onConfirm:se,children:e("p",{children:t("The new look for {name} has not been saved.",{name:a.displayName})})})]})}var Y=(a)=>t(ue[a]);function be(a){let l=a.base.colors,h=q(()=>Lt({class:a.id,colors:l}),[a.id,l]);return e(te,{data:{"data-class":a.id},spec:h,size:56,label:N("class",a.id),hint:V(a.id),pressed:a.pressed,onPick:a.onPick})}export{He as CharacterCreator};