pi-roundtable-web 0.23.1 → 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.
- package/CHANGELOG.md +61 -0
- package/README.md +90 -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 +161 -0
- package/src/app/app.ts +28 -0
- package/src/app/rooms.ts +7 -1
- package/src/app/routes.ts +223 -1
- package/src/app/workspace-files.ts +1485 -0
- package/src/console-api.ts +5 -0
- package/src/i18n/zh-tw.ts +185 -24
- package/src/options.ts +182 -5
- package/src/push/mutes.ts +68 -0
- package/src/push/quiet-hours.ts +60 -0
- package/src/push/triggers.ts +40 -9
- package/src/web-plugin.ts +46 -2
- package/dist/chunk-character-creator-660d2t6p.js +0 -1
- package/dist/chunk-connectors-d19v8cjc.js +0 -1
- package/dist/chunk-conversations-4cmexmqq.js +0 -1
- package/dist/chunk-entry-8xpx35ef.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-9tvf6es7.js +0 -41
- package/dist/chunk-index-p8h0e861.js +0 -2
- package/dist/chunk-index-xw5hhmfq.js +0 -2
- package/dist/chunk-notes-fefwds14.js +0 -1
- package/dist/chunk-overview-bxpwpmn6.js +0 -1
- package/dist/chunk-skills-pce3zgxy.js +0 -1
- package/dist/chunk-table-zmpbrfvz.js +0 -1
- package/dist/chunk-table2d-ac2gez71.js +0 -1
- package/dist/chunk-table3d-g258jemr.js +0 -4229
- package/dist/chunk-transcript-yhtjp3pd.js +0 -1
- package/dist/chunk-zh-tw-anqhqpaz.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,10 +76,14 @@ 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. */
|
|
67
84
|
authMode: SessionView["auth"];
|
|
85
|
+
/** The quiet hours of push, as the settings page shows them. */
|
|
86
|
+
quietHours?: NonNullable<SessionView["quietHours"]>;
|
|
68
87
|
/** Called after a change the other pages should refetch. */
|
|
69
88
|
changed(): void;
|
|
70
89
|
logger: Logger;
|
|
@@ -83,6 +102,78 @@ const bad = (message: string) => new HttpError(400, message);
|
|
|
83
102
|
const notFound = () =>
|
|
84
103
|
new HttpError(404, "There is no such room, agent or file.");
|
|
85
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
|
+
|
|
86
177
|
/** The parts of a path under the mount, each decoded; undefined for one that is not valid. */
|
|
87
178
|
export function partsOf(path: string): string[] | undefined {
|
|
88
179
|
try {
|
|
@@ -152,6 +243,7 @@ export class AppApi {
|
|
|
152
243
|
"schedules",
|
|
153
244
|
"agents",
|
|
154
245
|
"models",
|
|
246
|
+
"workspace",
|
|
155
247
|
].includes(second ?? ""));
|
|
156
248
|
if (!mine) return undefined;
|
|
157
249
|
try {
|
|
@@ -227,6 +319,7 @@ export class AppApi {
|
|
|
227
319
|
}
|
|
228
320
|
if (area === "files" && method === "GET" && id && !action)
|
|
229
321
|
return this.#file(id);
|
|
322
|
+
if (area === "workspace") return this.#workspace(request, parts, admission);
|
|
230
323
|
if (area === "schedules") return this.#schedules(method, id, admission);
|
|
231
324
|
if (area === "agents" && id)
|
|
232
325
|
return this.#agent(request, id, action, admission);
|
|
@@ -240,6 +333,9 @@ export class AppApi {
|
|
|
240
333
|
surface: this.#options.surface.surface,
|
|
241
334
|
auth: this.#options.authMode,
|
|
242
335
|
push: true,
|
|
336
|
+
...(this.#options.quietHours
|
|
337
|
+
? { quietHours: this.#options.quietHours }
|
|
338
|
+
: {}),
|
|
243
339
|
...(admission.expiresAt
|
|
244
340
|
? { expiresAt: admission.expiresAt.toISOString() }
|
|
245
341
|
: {}),
|
|
@@ -253,7 +349,7 @@ export class AppApi {
|
|
|
253
349
|
): Promise<Response> {
|
|
254
350
|
const { method } = request;
|
|
255
351
|
const [, , key, action] = parts;
|
|
256
|
-
const { rooms, log, cards, modes, conversations } = this.#options;
|
|
352
|
+
const { rooms, log, cards, modes, mutes, conversations } = this.#options;
|
|
257
353
|
if (!key) {
|
|
258
354
|
if (method !== "GET") throw new HttpError(405, "Method not allowed.");
|
|
259
355
|
return json({ rooms: await rooms.list() } satisfies RoomsView);
|
|
@@ -308,6 +404,17 @@ export class AppApi {
|
|
|
308
404
|
await conversations.startFresh(room.key);
|
|
309
405
|
return json({ ok: true });
|
|
310
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
|
+
}
|
|
311
418
|
throw new HttpError(404, "There is no such API.");
|
|
312
419
|
}
|
|
313
420
|
|
|
@@ -319,6 +426,121 @@ export class AppApi {
|
|
|
319
426
|
return fileResponse(meta, blob);
|
|
320
427
|
}
|
|
321
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
|
+
|
|
322
544
|
async #schedules(
|
|
323
545
|
method: string,
|
|
324
546
|
id: string | undefined,
|