pi-roundtable-web 0.23.2 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +82 -16
- package/dist/chunk-action-menu-7v5fce2v.js +1 -0
- package/dist/chunk-agent-2rw4ydw2.js +1 -0
- package/dist/chunk-agent-sheet-2q24dkkf.js +3 -0
- package/dist/chunk-character-creator-8q6ympxj.js +1 -0
- package/dist/chunk-codeeditor-fgmq298t.js +2 -0
- package/dist/chunk-connectors-mr53r7fh.js +1 -0
- package/dist/chunk-conversations-4vy2z6w9.js +1 -0
- package/dist/chunk-entry-jg1eem82.js +1 -0
- package/dist/chunk-group-scene-na95b1te.js +1 -0
- package/dist/chunk-index-11rjg8a8.js +1 -0
- package/dist/chunk-index-1y50d6sj.js +2 -0
- package/dist/chunk-index-5y2r0eaj.js +47 -0
- package/dist/chunk-index-6t68gbex.js +2 -0
- package/dist/chunk-index-7fphxxzd.js +14 -0
- package/dist/chunk-index-80f1zkp6.css +1 -0
- package/dist/chunk-index-87h45frg.js +2 -0
- package/dist/chunk-index-bnwmssyn.js +2 -0
- package/dist/chunk-index-ckgbszwt.js +51 -0
- package/dist/chunk-index-fmjjekhd.js +2 -0
- package/dist/chunk-index-mpm90318.js +2 -0
- package/dist/chunk-index-t1m65yn6.js +13 -0
- package/dist/chunk-index-t711t17m.js +2 -0
- package/dist/chunk-index-txv7qvyy.js +2 -0
- package/dist/chunk-index-wr3dg24d.js +2 -0
- package/dist/chunk-index-yx683d5n.js +2 -0
- package/dist/chunk-lang-css-fkdg3x8x.js +1 -0
- package/dist/chunk-lang-dockerfile-ey0nvhhm.js +1 -0
- package/dist/chunk-lang-go-403qsw9f.js +7 -0
- package/dist/chunk-lang-html-v8h9g977.js +1 -0
- package/dist/chunk-lang-javascript-ykb2vhaa.js +1 -0
- package/dist/chunk-lang-json-2rze3rpy.js +1 -0
- package/dist/chunk-lang-markdown-6w2htnfc.js +6 -0
- package/dist/chunk-lang-python-9jk5a49y.js +3 -0
- package/dist/chunk-lang-rust-24fggcqx.js +1 -0
- package/dist/chunk-lang-shell-cgq6q0kj.js +1 -0
- package/dist/chunk-lang-sql-gjywb9ke.js +2 -0
- package/dist/chunk-lang-toml-xh45nm68.js +1 -0
- package/dist/chunk-lang-typescript-qh6rebyb.js +1 -0
- package/dist/chunk-lang-xml-1mh879j9.js +1 -0
- package/dist/chunk-lang-yaml-jac98srt.js +1 -0
- package/dist/chunk-menu-items-zjpyj51b.js +2 -0
- package/dist/chunk-notes-sjba7kw3.js +1 -0
- package/dist/chunk-overview-26r77kb6.js +1 -0
- package/dist/chunk-room-confirms-851184ep.js +1 -0
- package/dist/chunk-room-menu-8y9c026y.js +1 -0
- package/dist/chunk-skills-56s4hdbn.js +1 -0
- package/dist/chunk-table-qykpvp32.js +1 -0
- package/dist/chunk-table2d-k60ressj.js +1 -0
- package/dist/chunk-table3d-bv2ajk39.js +4220 -0
- package/dist/chunk-transcript-vekz1mh6.js +1 -0
- package/dist/chunk-workspace-hevefd0d.js +1 -0
- package/dist/chunk-zh-tw-5r2wjre0.js +1 -0
- package/dist/index.html +2 -1
- package/dist/sw.js +1 -1
- package/package.json +21 -3
- package/src/api-types.ts +152 -0
- package/src/app/app.ts +11 -0
- package/src/app/rooms.ts +7 -1
- package/src/app/routes.ts +218 -1
- package/src/app/workspace-files.ts +1485 -0
- package/src/console-api.ts +5 -0
- package/src/i18n/zh-tw.ts +183 -24
- package/src/options.ts +148 -5
- package/src/push/mutes.ts +68 -0
- package/src/push/triggers.ts +30 -11
- package/src/web-plugin.ts +42 -2
- package/dist/chunk-character-creator-dwk6y4be.js +0 -1
- package/dist/chunk-connectors-pk0fh4y6.js +0 -1
- package/dist/chunk-conversations-sfsgmsdm.js +0 -1
- package/dist/chunk-entry-x0j9g57j.js +0 -1
- package/dist/chunk-index-19g8jmr8.js +0 -2
- package/dist/chunk-index-362dfedj.js +0 -1
- package/dist/chunk-index-6g69tqt0.js +0 -47
- package/dist/chunk-index-89q2qdkj.css +0 -1
- package/dist/chunk-index-9k42hnam.js +0 -41
- package/dist/chunk-index-k0z1ykph.js +0 -2
- package/dist/chunk-index-p8h0e861.js +0 -2
- package/dist/chunk-notes-v60aswry.js +0 -1
- package/dist/chunk-overview-c1c0ccda.js +0 -1
- package/dist/chunk-skills-r8rwbc5y.js +0 -1
- package/dist/chunk-table-pmr7zrgm.js +0 -1
- package/dist/chunk-table2d-ac2gez71.js +0 -1
- package/dist/chunk-table3d-g258jemr.js +0 -4229
- package/dist/chunk-transcript-wjqk37f5.js +0 -1
- 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,
|