pi-roundtable-web 0.23.2 → 0.24.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 (87) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +82 -16
  3. package/dist/chunk-action-menu-7v5fce2v.js +1 -0
  4. package/dist/chunk-agent-2rw4ydw2.js +1 -0
  5. package/dist/chunk-agent-sheet-2q24dkkf.js +3 -0
  6. package/dist/chunk-character-creator-8q6ympxj.js +1 -0
  7. package/dist/chunk-codeeditor-fgmq298t.js +2 -0
  8. package/dist/chunk-connectors-mr53r7fh.js +1 -0
  9. package/dist/chunk-conversations-4vy2z6w9.js +1 -0
  10. package/dist/chunk-entry-jg1eem82.js +1 -0
  11. package/dist/chunk-group-scene-na95b1te.js +1 -0
  12. package/dist/chunk-index-11rjg8a8.js +1 -0
  13. package/dist/chunk-index-1y50d6sj.js +2 -0
  14. package/dist/chunk-index-5y2r0eaj.js +47 -0
  15. package/dist/chunk-index-6t68gbex.js +2 -0
  16. package/dist/chunk-index-7fphxxzd.js +14 -0
  17. package/dist/chunk-index-80f1zkp6.css +1 -0
  18. package/dist/chunk-index-87h45frg.js +2 -0
  19. package/dist/chunk-index-bnwmssyn.js +2 -0
  20. package/dist/chunk-index-ckgbszwt.js +51 -0
  21. package/dist/chunk-index-fmjjekhd.js +2 -0
  22. package/dist/chunk-index-mpm90318.js +2 -0
  23. package/dist/chunk-index-t1m65yn6.js +13 -0
  24. package/dist/chunk-index-t711t17m.js +2 -0
  25. package/dist/chunk-index-txv7qvyy.js +2 -0
  26. package/dist/chunk-index-wr3dg24d.js +2 -0
  27. package/dist/chunk-index-yx683d5n.js +2 -0
  28. package/dist/chunk-lang-css-fkdg3x8x.js +1 -0
  29. package/dist/chunk-lang-dockerfile-ey0nvhhm.js +1 -0
  30. package/dist/chunk-lang-go-403qsw9f.js +7 -0
  31. package/dist/chunk-lang-html-v8h9g977.js +1 -0
  32. package/dist/chunk-lang-javascript-ykb2vhaa.js +1 -0
  33. package/dist/chunk-lang-json-2rze3rpy.js +1 -0
  34. package/dist/chunk-lang-markdown-6w2htnfc.js +6 -0
  35. package/dist/chunk-lang-python-9jk5a49y.js +3 -0
  36. package/dist/chunk-lang-rust-24fggcqx.js +1 -0
  37. package/dist/chunk-lang-shell-cgq6q0kj.js +1 -0
  38. package/dist/chunk-lang-sql-gjywb9ke.js +2 -0
  39. package/dist/chunk-lang-toml-xh45nm68.js +1 -0
  40. package/dist/chunk-lang-typescript-qh6rebyb.js +1 -0
  41. package/dist/chunk-lang-xml-1mh879j9.js +1 -0
  42. package/dist/chunk-lang-yaml-jac98srt.js +1 -0
  43. package/dist/chunk-menu-items-zjpyj51b.js +2 -0
  44. package/dist/chunk-notes-sjba7kw3.js +1 -0
  45. package/dist/chunk-overview-26r77kb6.js +1 -0
  46. package/dist/chunk-room-confirms-851184ep.js +1 -0
  47. package/dist/chunk-room-menu-8y9c026y.js +1 -0
  48. package/dist/chunk-skills-56s4hdbn.js +1 -0
  49. package/dist/chunk-table-qykpvp32.js +1 -0
  50. package/dist/chunk-table2d-k60ressj.js +1 -0
  51. package/dist/chunk-table3d-bv2ajk39.js +4220 -0
  52. package/dist/chunk-transcript-vekz1mh6.js +1 -0
  53. package/dist/chunk-workspace-hevefd0d.js +1 -0
  54. package/dist/chunk-zh-tw-5r2wjre0.js +1 -0
  55. package/dist/index.html +2 -1
  56. package/dist/sw.js +1 -1
  57. package/package.json +21 -3
  58. package/src/api-types.ts +152 -0
  59. package/src/app/app.ts +11 -0
  60. package/src/app/rooms.ts +7 -1
  61. package/src/app/routes.ts +218 -1
  62. package/src/app/workspace-files.ts +1485 -0
  63. package/src/console-api.ts +5 -0
  64. package/src/i18n/zh-tw.ts +183 -24
  65. package/src/options.ts +148 -5
  66. package/src/push/mutes.ts +68 -0
  67. package/src/push/triggers.ts +30 -11
  68. package/src/web-plugin.ts +42 -2
  69. package/dist/chunk-character-creator-dwk6y4be.js +0 -1
  70. package/dist/chunk-connectors-pk0fh4y6.js +0 -1
  71. package/dist/chunk-conversations-sfsgmsdm.js +0 -1
  72. package/dist/chunk-entry-x0j9g57j.js +0 -1
  73. package/dist/chunk-index-19g8jmr8.js +0 -2
  74. package/dist/chunk-index-362dfedj.js +0 -1
  75. package/dist/chunk-index-6g69tqt0.js +0 -47
  76. package/dist/chunk-index-89q2qdkj.css +0 -1
  77. package/dist/chunk-index-9k42hnam.js +0 -41
  78. package/dist/chunk-index-k0z1ykph.js +0 -2
  79. package/dist/chunk-index-p8h0e861.js +0 -2
  80. package/dist/chunk-notes-v60aswry.js +0 -1
  81. package/dist/chunk-overview-c1c0ccda.js +0 -1
  82. package/dist/chunk-skills-r8rwbc5y.js +0 -1
  83. package/dist/chunk-table-pmr7zrgm.js +0 -1
  84. package/dist/chunk-table2d-ac2gez71.js +0 -1
  85. package/dist/chunk-table3d-g258jemr.js +0 -4229
  86. package/dist/chunk-transcript-wjqk37f5.js +0 -1
  87. package/dist/chunk-zh-tw-mtvm7sn7.js +0 -1
package/src/app/routes.ts CHANGED
@@ -26,9 +26,16 @@ import type {
26
26
  SchedulesView,
27
27
  ScheduleView,
28
28
  SessionView,
29
+ WorkspaceConflict,
30
+ WorkspaceNew,
31
+ WorkspaceRefusalBody,
32
+ WorkspaceRename,
33
+ WorkspaceRootsView,
34
+ WorkspaceSave,
29
35
  } from "../api-types.ts";
30
36
  import type { Authenticated } from "../auth/authenticator.ts";
31
37
  import { json } from "../http.ts";
38
+ import type { PushMutes } from "../push/mutes.ts";
32
39
  import { readLimited } from "../request-body.ts";
33
40
  import type { CardLog } from "./card-log.ts";
34
41
  import { type FileDir, fileResponse, isFileId } from "./files.ts";
@@ -36,6 +43,12 @@ import { PAGE_LIMIT, type RoomLog } from "./room-log.ts";
36
43
  import type { Rooms } from "./rooms.ts";
37
44
  import type { AppSurface } from "./surface.ts";
38
45
  import { UploadRefusal, type Uploads } from "./uploads.ts";
46
+ import {
47
+ EDIT_MAX_BYTES,
48
+ WorkspaceConflictError,
49
+ type WorkspaceFiles,
50
+ WorkspaceRefusal,
51
+ } from "./workspace-files.ts";
39
52
 
40
53
  /** The largest JSON body a write reads: 64 KiB. */
41
54
  const MAX_BODY_BYTES = 64 * 1024;
@@ -51,6 +64,8 @@ export interface AppApiOptions {
51
64
  /** Where stored files are read from. */
52
65
  files: FileDir;
53
66
  modes: Pick<ConversationModes, "set" | "peek">;
67
+ /** The rooms whose push the owners muted. */
68
+ mutes: Pick<PushMutes, "set">;
54
69
  conversations: Pick<ConversationPort, "startFresh">;
55
70
  identity: Pick<IdentityService, "tierOf">;
56
71
  /** Present when the schedules pane is served. */
@@ -61,6 +76,8 @@ export interface AppApiOptions {
61
76
  "update" | "dress" | "usableModels" | "modelOf" | "defaultModel"
62
77
  >;
63
78
  avatars: Pick<AvatarStudio, "serve">;
79
+ /** Present when the workspace pane is served. */
80
+ workspace?: WorkspaceFiles;
64
81
  /** The mount, which a room's agent picture hangs under. */
65
82
  mount: string;
66
83
  /** How the console signs the person in, as the session reports it. */
@@ -85,6 +102,78 @@ const bad = (message: string) => new HttpError(400, message);
85
102
  const notFound = () =>
86
103
  new HttpError(404, "There is no such room, agent or file.");
87
104
 
105
+ /** A refusal of the workspace pane as the HTTP error the owner reads. */
106
+ function refusalOf(refusal: WorkspaceRefusal): HttpError {
107
+ const { status } = refusal;
108
+ switch (refusal.reason) {
109
+ case "bad-path":
110
+ return new HttpError(status, "That path is not allowed.");
111
+ case "unknown-root":
112
+ return new HttpError(status, "There is no such place.");
113
+ case "not-found":
114
+ return new HttpError(status, "There is no such file or folder.");
115
+ case "not-a-dir":
116
+ return new HttpError(status, "That is not a folder.");
117
+ case "is-a-dir":
118
+ return new HttpError(status, "That is a folder, not a file.");
119
+ case "is-root":
120
+ return new HttpError(
121
+ status,
122
+ "That is the top folder, which cannot be changed.",
123
+ );
124
+ case "outside":
125
+ return new HttpError(status, "That points outside the workspace.");
126
+ case "special":
127
+ return new HttpError(status, "That is not a regular file.");
128
+ case "symlink":
129
+ return new HttpError(
130
+ status,
131
+ "That is a link; open the file it points to.",
132
+ );
133
+ case "read-only-root":
134
+ return new HttpError(status, "This place is read-only.");
135
+ case "git-dir":
136
+ return new HttpError(status, "Files inside .git are read-only.");
137
+ case "trash-dir":
138
+ return new HttpError(status, "The trash cannot be changed this way.");
139
+ case "binary":
140
+ return new HttpError(
141
+ status,
142
+ "This file is not text, so it cannot be edited.",
143
+ );
144
+ case "not-utf8":
145
+ return new HttpError(
146
+ status,
147
+ "This file is not UTF-8 text, so it cannot be edited.",
148
+ );
149
+ case "too-large":
150
+ return new HttpError(status, "This file is too large.");
151
+ case "text-too-large":
152
+ return new HttpError(
153
+ status,
154
+ "The text is too large to save (1 MiB at most).",
155
+ );
156
+ case "into-itself":
157
+ return new HttpError(status, "A folder cannot be moved into itself.");
158
+ case "cross-device":
159
+ return new HttpError(status, "That cannot be moved to another disk.");
160
+ case "permission":
161
+ return new HttpError(status, "The server is not allowed to change that.");
162
+ }
163
+ }
164
+
165
+ /** A conflict of the workspace pane as the HTTP error the owner reads. */
166
+ function conflictOf(conflict: WorkspaceConflictError): HttpError {
167
+ switch (conflict.conflict) {
168
+ case "changed":
169
+ return new HttpError(409, "This file changed while you were editing.");
170
+ case "gone":
171
+ return new HttpError(409, "This file is gone.");
172
+ case "exists":
173
+ return new HttpError(409, "Something with that name is already there.");
174
+ }
175
+ }
176
+
88
177
  /** The parts of a path under the mount, each decoded; undefined for one that is not valid. */
89
178
  export function partsOf(path: string): string[] | undefined {
90
179
  try {
@@ -154,6 +243,7 @@ export class AppApi {
154
243
  "schedules",
155
244
  "agents",
156
245
  "models",
246
+ "workspace",
157
247
  ].includes(second ?? ""));
158
248
  if (!mine) return undefined;
159
249
  try {
@@ -229,6 +319,7 @@ export class AppApi {
229
319
  }
230
320
  if (area === "files" && method === "GET" && id && !action)
231
321
  return this.#file(id);
322
+ if (area === "workspace") return this.#workspace(request, parts, admission);
232
323
  if (area === "schedules") return this.#schedules(method, id, admission);
233
324
  if (area === "agents" && id)
234
325
  return this.#agent(request, id, action, admission);
@@ -258,7 +349,7 @@ export class AppApi {
258
349
  ): Promise<Response> {
259
350
  const { method } = request;
260
351
  const [, , key, action] = parts;
261
- const { rooms, log, cards, modes, conversations } = this.#options;
352
+ const { rooms, log, cards, modes, mutes, conversations } = this.#options;
262
353
  if (!key) {
263
354
  if (method !== "GET") throw new HttpError(405, "Method not allowed.");
264
355
  return json({ rooms: await rooms.list() } satisfies RoomsView);
@@ -313,6 +404,17 @@ export class AppApi {
313
404
  await conversations.startFresh(room.key);
314
405
  return json({ ok: true });
315
406
  }
407
+ if (action === "mute" && method === "PUT" && parts.length === 4) {
408
+ // Only an agent's or group's room has a mute of its own; a thread's posts are its parent's business.
409
+ if (room.readOnly) throw notFound();
410
+ const owner = await this.#owner(admission);
411
+ const { muted } = await bodyOf(request);
412
+ if (typeof muted !== "boolean") throw bad("muted must be true or false.");
413
+ await mutes.set(room.key, muted, owner.principalId);
414
+ // The other pages and devices read the room list again to show the bell.
415
+ this.#options.changed();
416
+ return json({ muted });
417
+ }
316
418
  throw new HttpError(404, "There is no such API.");
317
419
  }
318
420
 
@@ -324,6 +426,121 @@ export class AppApi {
324
426
  return fileResponse(meta, blob);
325
427
  }
326
428
 
429
+ /** The workspace files pane's API; every call, a read too, is an owner's. */
430
+ async #workspace(
431
+ request: Request,
432
+ parts: string[],
433
+ admission: Authenticated,
434
+ ): Promise<Response> {
435
+ const files = this.#options.workspace;
436
+ if (!files) throw new HttpError(404, "This pane is not served.");
437
+ const owner = await this.#owner(admission);
438
+ return files.speaking(owner.id, () =>
439
+ this.#workspaceCall(request, parts, files, owner.id),
440
+ );
441
+ }
442
+
443
+ async #workspaceCall(
444
+ request: Request,
445
+ parts: string[],
446
+ files: WorkspaceFiles,
447
+ by: string,
448
+ ): Promise<Response> {
449
+ const { method } = request;
450
+ const [, , rootId, action, trashId, trashAction] = parts;
451
+ const params = new URL(request.url).searchParams;
452
+ const path = params.get("path") ?? "";
453
+ try {
454
+ if (!rootId) {
455
+ if (method !== "GET") throw new HttpError(405, "Method not allowed.");
456
+ return json({ roots: files.roots() } satisfies WorkspaceRootsView);
457
+ }
458
+ if (action === "list" && method === "GET" && parts.length === 4)
459
+ return json(await files.list(rootId, path));
460
+ if (action === "raw" && method === "GET" && parts.length === 4)
461
+ return await files.raw(rootId, path, params.get("download") === "1", {
462
+ range: request.headers.get("range"),
463
+ ifRange: request.headers.get("if-range"),
464
+ ifNoneMatch: request.headers.get("if-none-match"),
465
+ });
466
+ if (action === "file" && parts.length === 4) {
467
+ if (method === "GET") return json(await files.read(rootId, path));
468
+ if (method === "PUT") {
469
+ const body = await bodyOf(request, EDIT_MAX_BYTES * 2 + 4096);
470
+ const { text, baseHash } = body;
471
+ if (typeof text !== "string") throw bad("text must be text.");
472
+ if (typeof baseHash !== "string" || !/^[0-9a-f]{64}$/.test(baseHash))
473
+ throw bad("baseHash must be the hash the file was read with.");
474
+ const save: WorkspaceSave = { text, baseHash };
475
+ return json(await files.save(rootId, path, save, by));
476
+ }
477
+ }
478
+ if (action === "new" && method === "POST" && parts.length === 4) {
479
+ const body = await bodyOf(request, EDIT_MAX_BYTES * 2 + 4096);
480
+ const { kind, text } = body;
481
+ if (kind !== "file" && kind !== "dir")
482
+ throw bad("kind must be file or dir.");
483
+ if (text !== undefined && typeof text !== "string")
484
+ throw bad("text must be text.");
485
+ const made: WorkspaceNew = {
486
+ kind,
487
+ ...(text === undefined ? {} : { text }),
488
+ };
489
+ return json(await files.create(rootId, path, made, by), 201);
490
+ }
491
+ if (action === "rename" && method === "POST" && parts.length === 4) {
492
+ const body = await bodyOf(request);
493
+ const { from, to } = body;
494
+ if (typeof from !== "string" || typeof to !== "string")
495
+ throw bad("from and to must be paths.");
496
+ const moved: WorkspaceRename = { from, to };
497
+ await files.rename(rootId, moved.from, moved.to, by);
498
+ return json({ ok: true });
499
+ }
500
+ if (action === "entry" && method === "DELETE" && parts.length === 4)
501
+ return json(await files.remove(rootId, path, by));
502
+ if (action === "trash") {
503
+ if (method === "GET" && !trashId)
504
+ return json(await files.trash(rootId));
505
+ if (
506
+ method === "POST" &&
507
+ trashId &&
508
+ trashAction === "restore" &&
509
+ parts.length === 6
510
+ )
511
+ return json(await files.restore(rootId, trashId, by));
512
+ if (method === "DELETE" && trashId && !trashAction) {
513
+ await files.purge(rootId, trashId, by);
514
+ return json({ ok: true });
515
+ }
516
+ }
517
+ } catch (error) {
518
+ if (error instanceof WorkspaceRefusal) {
519
+ const refused = refusalOf(error);
520
+ return json(
521
+ {
522
+ error: refused.message,
523
+ reason: error.reason,
524
+ } satisfies WorkspaceRefusalBody,
525
+ refused.status,
526
+ );
527
+ }
528
+ if (error instanceof WorkspaceConflictError) {
529
+ const refused = conflictOf(error);
530
+ return json(
531
+ {
532
+ error: refused.message,
533
+ conflict: error.conflict,
534
+ ...(error.current ? { current: error.current } : {}),
535
+ } satisfies WorkspaceConflict,
536
+ 409,
537
+ );
538
+ }
539
+ throw error;
540
+ }
541
+ throw new HttpError(404, "There is no such API.");
542
+ }
543
+
327
544
  async #schedules(
328
545
  method: string,
329
546
  id: string | undefined,