copilotz 1.0.0-rc.1 → 1.0.0-rc.2
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 +17 -0
- package/README.md +22 -9
- package/hosts/orgs/mod.d.ts +10 -3
- package/hosts/orgs/mod.js +21 -7
- package/hosts/web/host.js +7 -2
- package/hosts/web/node/serve.js +3 -1
- package/hosts/web/server.d.ts +7 -0
- package/hosts/web/server.js +13 -1
- package/package.json +1 -1
- package/plugins/agents/chat/views/web/home.js +1 -1
- package/plugins/agents/chat/views/web/who.d.ts +3 -2
- package/plugins/agents/chat/views/web/who.js +6 -3
- package/plugins/compute/computers/actions/code.js +7 -1
- package/plugins/compute/computers/program/release.d.ts +10 -10
- package/plugins/compute/computers/program/release.js +7 -4
- package/plugins/compute/computers/program/version.d.ts +2 -0
- package/plugins/compute/computers/program/version.js +2 -0
- package/plugins/compute/computers/resources/commands.js +4 -4
- package/plugins/compute/computers/resources/downloads.d.ts +6 -2
- package/plugins/compute/computers/resources/downloads.js +15 -7
- package/plugins/compute/computers/shared/folder.d.ts +3 -1
- package/plugins/compute/computers/shared/folder.js +7 -3
- package/plugins/compute/computers/views/web/connect.js +14 -6
- package/plugins/interfaces/web/views/frame.d.ts +3 -1
- package/plugins/interfaces/web/views/frame.js +2 -2
- package/plugins/interfaces/web/views/shell.js +2 -1
- package/runtime/define.d.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## 1.0.0-rc.2 — 2026-10-30
|
|
2
|
+
|
|
3
|
+
- **Orgs:** an org's socket is in its own address (`/o/<org>/oxian`), beside the shared `/oxian`, so
|
|
4
|
+
a browser that sends no cookies with a WebSocket (Safari) reaches its org. The web host's page
|
|
5
|
+
says which socket to use (`socket`) and its base (`base`).
|
|
6
|
+
- **Web:** a person shows as their name, from their profile, not as their id: the viewer has a
|
|
7
|
+
`label` beside its `name`, which the shell's account menu and the chat's greeting use.
|
|
8
|
+
- **Computers:** one machine serves many apps, or many orgs of one. Each is a computer of its own
|
|
9
|
+
there, with its own id and secret, all in one program; a state kept before becomes its one
|
|
10
|
+
connection. `computer list` says which and where; `computer disconnect <app-url>` leaves one. An
|
|
11
|
+
org's spaces go in a folder named for the org (`~/Copilotz/acme-studio`, `~/Copilotz/personal`): a
|
|
12
|
+
code made on an org says it (`folder`), and the install lines pass it on (`--folder`).
|
|
13
|
+
- **The computer program's release:** each release on GitHub has it, made by the publish workflow on
|
|
14
|
+
a Mac. When the program is the same as the last release's (`source` in its manifest), the release
|
|
15
|
+
has that one's binaries, version and all, so computers don't download what they have.
|
|
16
|
+
`computerDownloads()` serves this Copilotz's own release by default.
|
|
17
|
+
|
|
1
18
|
## 1.0.0-rc.1 — 2026-10-30
|
|
2
19
|
|
|
3
20
|
Copilotz 1.0 is a new runtime, written from scratch: one runtime for agents and the apps around
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ bun add copilotz # Bun
|
|
|
22
22
|
deno add jsr:@copilotz/copilotz # Deno (or `deno add npm:copilotz`)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
On Deno, name it `copilotz` in `deno.json` (`"copilotz": "jsr:@copilotz/copilotz@^1.0.0-rc.
|
|
25
|
+
On Deno, name it `copilotz` in `deno.json` (`"copilotz": "jsr:@copilotz/copilotz@^1.0.0-rc.2"`), and
|
|
26
26
|
the imports below are the same everywhere. Some pieces need a package of their own, installed beside
|
|
27
27
|
it: `esbuild` to bundle the web host on Node, `@electric-sql/pglite` for PostgreSQL in the process,
|
|
28
28
|
and `@types/react` for the front ends' types.
|
|
@@ -196,18 +196,26 @@ each socket to the runtime of the org its request is for:
|
|
|
196
196
|
## Installing a computer
|
|
197
197
|
|
|
198
198
|
People's computers run the computer program (`plugins/compute/computers/program/`), a single
|
|
199
|
-
executable for macOS, Linux and Windows
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
node scripts/computer/build.ts # dist/computer/<version>/: a binary per platform (by Bun),
|
|
203
|
-
# install.sh, install.ps1, manifest.json (sizes, SHA-256)
|
|
204
|
-
```
|
|
199
|
+
executable for macOS, Linux and Windows. Each Copilotz release on GitHub has it (`v<version>`: a
|
|
200
|
+
binary per platform, `install.sh`, `install.ps1`, and `manifest.json` with their sizes and SHA-256),
|
|
201
|
+
and an app serves it from there by default:
|
|
205
202
|
|
|
206
203
|
```ts
|
|
207
204
|
import { computerDownloads } from "copilotz/plugins/compute/downloads";
|
|
208
205
|
|
|
209
|
-
plugins: [...plugins, await computerDownloads(
|
|
210
|
-
plugins: [...plugins, await computerDownloads({
|
|
206
|
+
plugins: [...plugins, await computerDownloads()], // this Copilotz's release
|
|
207
|
+
plugins: [...plugins, await computerDownloads({ dir: "dist/computer/1.0.0-rc.2" })], // its own: /computer/download/
|
|
208
|
+
plugins: [...plugins, await computerDownloads({ url: "https://downloads.example.com/computer/" })], // or elsewhere
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The publish workflow makes it when a `v1.*` tag is pushed, on a Mac (the macOS binaries are signed
|
|
212
|
+
there). A release whose program is the same as the last one's (`source` in the manifest: a hash of
|
|
213
|
+
its code, without its version, its install scripts and the Bun that builds it) has that release's
|
|
214
|
+
binaries, version and all, so computers don't update to what they have. To build one yourself:
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
node scripts/computer/build.ts # dist/computer/<version>/
|
|
218
|
+
node scripts/computer/build.ts --reuse <url> # that release's, when it's the same program
|
|
211
219
|
```
|
|
212
220
|
|
|
213
221
|
A space's Computer tab (or `/computer connect`) then gives the one line to run on the machine, for
|
|
@@ -232,6 +240,11 @@ and its binary's URL, size and SHA-256; it checks them, and that the new one run
|
|
|
232
240
|
starts again. The binaries aren't signed yet, so macOS and Windows warn about one downloaded with a
|
|
233
241
|
browser; the line doesn't need that.
|
|
234
242
|
|
|
243
|
+
One machine can serve several apps, or several orgs of one: run the line from each, and each is a
|
|
244
|
+
computer of its own there, in one program. Each one's spaces go in a folder of its own under
|
|
245
|
+
`~/Copilotz`, named for the org (the line from an org says it: `--folder acme`), else for the app's
|
|
246
|
+
host. `computer list` says which and where; `computer disconnect <app-url>` leaves one.
|
|
247
|
+
|
|
235
248
|
## Plugins
|
|
236
249
|
|
|
237
250
|
Plugins come in themes. Every plugin is a plain object, and its settings are a resource under its
|
package/hosts/orgs/mod.d.ts
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* - `POST /signout`: out of the directory and every org.
|
|
20
20
|
* - `/o/<org>/…`: that org, by its address, for what reaches it without a
|
|
21
21
|
* browser's session: its API (a key), its webhooks (signed), its
|
|
22
|
-
* computers'
|
|
22
|
+
* computers' downloads. The org's own handlers say who may.
|
|
23
|
+
* - `/o/<org>/oxian`: that org's socket, for its pages and its computers
|
|
24
|
+
* (a ticket or a computer's credentials say who); no cookie needed.
|
|
23
25
|
* - Staff's console: `GET /console/orgs`; `POST /console/orgs/<id>`
|
|
24
26
|
* `{ suspended?, name? }`; `POST …/<id>/move` `{ schema }` (its rows to
|
|
25
27
|
* another schema); `GET …/<id>/export` (its rows, as SQL); `POST
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
* its tenant), its plugins with the host's `here` (the org's name, and how
|
|
31
33
|
* it reaches the directory), and `admit` for its browsers.
|
|
32
34
|
*/
|
|
33
|
-
import type { Actor, Plugins, Runtime } from "../../runtime/mod.js";
|
|
35
|
+
import type { Actor, Plugins, Runtime, Transport } from "../../runtime/mod.js";
|
|
34
36
|
import { type Code, type WebServerOptions } from "../web/server.js";
|
|
35
37
|
/** An org as the host opens it. */
|
|
36
38
|
export type OrgInfo = {
|
|
@@ -44,10 +46,15 @@ export type OrgInfo = {
|
|
|
44
46
|
export type OrgsHostOptions = Pick<WebServerOptions, "title" | "theme" | "google" | "dev" | "assets"> & {
|
|
45
47
|
/** The directory's runtime: identity, a sign-in method, and the directory plugin. */
|
|
46
48
|
directory: Runtime;
|
|
47
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Opens an org: the app's runtime for it, with these plugins too, this
|
|
51
|
+
* `admit`, and these `transports` (its socket, at `/oxian` and at its
|
|
52
|
+
* address, `/o/<org>/oxian`).
|
|
53
|
+
*/
|
|
48
54
|
open(org: OrgInfo, given: {
|
|
49
55
|
plugins: Plugins;
|
|
50
56
|
admit: (ticket: string) => Actor | undefined;
|
|
57
|
+
transports: Transport[];
|
|
51
58
|
}): Promise<Runtime>;
|
|
52
59
|
/** The socket upgrade for an org's runtime (its transport for browsers). */
|
|
53
60
|
upgrade(runtime: Runtime, request: Request): Response | Promise<Response>;
|
package/hosts/orgs/mod.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { definePlugin } from "../../runtime/mod.js";
|
|
2
2
|
import { createTickets } from "../web/tickets.js";
|
|
3
|
-
import { cookieOf, setCookie, webServer } from "../web/server.js";
|
|
3
|
+
import { cookieOf, setCookie, socket, webServer, } from "../web/server.js";
|
|
4
4
|
const DIRECTORY = "dir";
|
|
5
5
|
const CURRENT = "org";
|
|
6
6
|
const MONTH = 30 * 86_400;
|
|
7
7
|
const sessionOf = (org) => `s_${org}`;
|
|
8
8
|
/** Where an org is reached by its address: `/o/<org>`. */
|
|
9
9
|
const baseOf = (org) => `/o/${encodeURIComponent(org)}`;
|
|
10
|
+
/**
|
|
11
|
+
* Where a page's or a computer's socket reaches an org: under its address
|
|
12
|
+
* (`/o/<org>/oxian`), which names it without cookies (Safari sends none with
|
|
13
|
+
* a socket). Its runtime takes sockets there and at `/oxian` (`given.transports`).
|
|
14
|
+
*/
|
|
15
|
+
const socketOf = (org) => `${baseOf(org)}${socket.config.path}`;
|
|
10
16
|
/** The org a path addresses (`/o/<org>/…`), and the path within it. */
|
|
11
17
|
const addressed = (path) => {
|
|
12
18
|
const found = /^\/o\/([^/]+)(\/.*)?$/.exec(path);
|
|
@@ -54,7 +60,11 @@ export function orgsHost(options) {
|
|
|
54
60
|
if (!cell) {
|
|
55
61
|
cell = (async () => {
|
|
56
62
|
const tickets = createTickets();
|
|
57
|
-
const runtime = await options.open(org, {
|
|
63
|
+
const runtime = await options.open(org, {
|
|
64
|
+
plugins: [here(org)],
|
|
65
|
+
admit: tickets.admit,
|
|
66
|
+
transports: [socket, { type: "websocket", config: { path: socketOf(org.id) } }],
|
|
67
|
+
});
|
|
58
68
|
const serve = webServer({
|
|
59
69
|
runtime,
|
|
60
70
|
tickets,
|
|
@@ -68,6 +78,7 @@ export function orgsHost(options) {
|
|
|
68
78
|
cookie: sessionOf(org.id),
|
|
69
79
|
signIn: false,
|
|
70
80
|
base: baseOf(org.id),
|
|
81
|
+
socket: socketOf(org.id),
|
|
71
82
|
});
|
|
72
83
|
const made = { org, runtime, tickets, serve, last: Date.now() };
|
|
73
84
|
live.set(org.id, made);
|
|
@@ -293,6 +304,9 @@ export function orgsHost(options) {
|
|
|
293
304
|
return json({ error: "This org is suspended." }, { status: 403 });
|
|
294
305
|
const cell = await cellOf(org);
|
|
295
306
|
cell.last = Date.now();
|
|
307
|
+
// Its socket, as it came: its runtime takes sockets at this address too.
|
|
308
|
+
if (at.path === socket.config.path)
|
|
309
|
+
return await options.upgrade(cell.runtime, request);
|
|
296
310
|
const inner = new URL(url);
|
|
297
311
|
inner.pathname = at.path;
|
|
298
312
|
return await cell.serve(new Request(inner, request));
|
|
@@ -475,12 +489,12 @@ export function orgsHost(options) {
|
|
|
475
489
|
* before its socket connects; one not open yet is a retry for the client.
|
|
476
490
|
*/
|
|
477
491
|
openRuntime(request) {
|
|
478
|
-
const
|
|
479
|
-
const org =
|
|
492
|
+
const named = addressed(new URL(request.url).pathname)?.org;
|
|
493
|
+
const org = named ?? cookieOf(request, CURRENT);
|
|
480
494
|
const cell = org ? live.get(org) : undefined;
|
|
481
|
-
// An org
|
|
482
|
-
if (!cell &&
|
|
483
|
-
void orgInfo(
|
|
495
|
+
// An org named but closed (a computer connecting first) opens for its retry.
|
|
496
|
+
if (!cell && named) {
|
|
497
|
+
void orgInfo(named).then((info) => {
|
|
484
498
|
if (info && !info.suspended)
|
|
485
499
|
void cellOf(info).catch(() => { });
|
|
486
500
|
}).catch(() => { });
|
package/hosts/web/host.js
CHANGED
|
@@ -96,7 +96,7 @@ export async function web(options) {
|
|
|
96
96
|
void loadOrgs();
|
|
97
97
|
let fresh = first.ticket;
|
|
98
98
|
const url = options.url ??
|
|
99
|
-
`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/oxian`;
|
|
99
|
+
`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${config().socket ?? "/oxian"}`;
|
|
100
100
|
const client = connect({
|
|
101
101
|
transport: { type: "websocket", config: { url, allowInsecureLoopback: true } },
|
|
102
102
|
// The first connection takes the ticket in hand; a new one asks again.
|
|
@@ -113,7 +113,12 @@ export async function web(options) {
|
|
|
113
113
|
plugins: options.plugins,
|
|
114
114
|
});
|
|
115
115
|
await client.ready;
|
|
116
|
-
|
|
116
|
+
// Who they are (their id), and what they're shown as (their name).
|
|
117
|
+
const viewer = {
|
|
118
|
+
kind: "person",
|
|
119
|
+
name: first.person,
|
|
120
|
+
...(first.name ? { label: first.name } : {}),
|
|
121
|
+
};
|
|
117
122
|
session.client = client;
|
|
118
123
|
session.viewer = viewer;
|
|
119
124
|
let space = (await options.start?.(client, first.person))?.space;
|
package/hosts/web/node/serve.js
CHANGED
|
@@ -20,7 +20,9 @@ export async function serve(options) {
|
|
|
20
20
|
export async function listen(options) {
|
|
21
21
|
const { hostname = "127.0.0.1", port = 0, signal } = options;
|
|
22
22
|
const prepare = (request) => {
|
|
23
|
-
|
|
23
|
+
// The socket's path, or a runtime's own under it (an org's: `/o/<org>/oxian`).
|
|
24
|
+
const path = new URL(request.url).pathname;
|
|
25
|
+
const to = path === socket.config.path || path.endsWith(socket.config.path)
|
|
24
26
|
? options.hypervisor(request)
|
|
25
27
|
: undefined;
|
|
26
28
|
return to ? to.prepare(request) : { kind: "response", response: options.fetch(request) };
|
package/hosts/web/server.d.ts
CHANGED
|
@@ -76,6 +76,13 @@ export type WebServerOptions = {
|
|
|
76
76
|
* The page tells its code (`appBase()`), so the addresses it shows work.
|
|
77
77
|
*/
|
|
78
78
|
base?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Where the page's socket connects, when it isn't the runtime's own
|
|
81
|
+
* `/oxian`: a host serving many orgs gives each org its address there
|
|
82
|
+
* (`/o/<org>/oxian`), so the socket finds it without cookies, which Safari
|
|
83
|
+
* doesn't send with a socket.
|
|
84
|
+
*/
|
|
85
|
+
socket?: string;
|
|
79
86
|
/**
|
|
80
87
|
* The app's theme, as its front ends give it (`themeOf(plugins)`, of those
|
|
81
88
|
* with a `theme` resource): the page carries it for its first paint.
|
package/hosts/web/server.js
CHANGED
|
@@ -103,6 +103,7 @@ JSON.stringify({
|
|
|
103
103
|
google: options.google ?? null,
|
|
104
104
|
dev: options.dev ?? false,
|
|
105
105
|
...(options.base ? { base: options.base } : {}),
|
|
106
|
+
...(options.socket ? { socket: options.socket } : {}),
|
|
106
107
|
})
|
|
107
108
|
.replaceAll("<", "\\u003c")}</script>
|
|
108
109
|
</head>
|
|
@@ -189,7 +190,18 @@ export function webServer(options) {
|
|
|
189
190
|
const actor = await whom(request);
|
|
190
191
|
if (!actor)
|
|
191
192
|
return json({ error: "Not signed in" }, { status: 401 });
|
|
192
|
-
|
|
193
|
+
// What the page shows them as: their name as they signed in, else their email.
|
|
194
|
+
const profile = actor.kind === "person"
|
|
195
|
+
? await runtime.query("person.profile", {
|
|
196
|
+
person: actor.id,
|
|
197
|
+
}).catch(() => null)
|
|
198
|
+
: null;
|
|
199
|
+
const name = profile?.name ?? profile?.email;
|
|
200
|
+
return json({
|
|
201
|
+
ticket: tickets.issue(actor),
|
|
202
|
+
person: actor.id,
|
|
203
|
+
...(name ? { name } : {}),
|
|
204
|
+
});
|
|
193
205
|
}
|
|
194
206
|
case "POST /signin/google": {
|
|
195
207
|
if (!options.google || !signIn)
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ export const homeView = (_, ctx) => {
|
|
|
19
19
|
const to = String(ctx.hints.query?.to ?? "");
|
|
20
20
|
const suggestions = Object.values((ctx.resources.suggestions ?? {}))
|
|
21
21
|
.filter((s) => typeof s === "string");
|
|
22
|
-
const name = greeted(ctx.viewer?.name ?? "");
|
|
22
|
+
const name = greeted(ctx.viewer?.label ?? ctx.viewer?.name ?? "");
|
|
23
23
|
const shared = space && space.kind !== "personal";
|
|
24
24
|
// What other front ends show over the greeting.
|
|
25
25
|
const tops = Object.values((ctx.resources.home ?? {}))
|
|
@@ -8,7 +8,8 @@ export declare const byAgent: (m: Message) => boolean;
|
|
|
8
8
|
/** Names as a sentence says them: "ana", "ana and bob", "ana, bob and cy". */
|
|
9
9
|
export declare const names: (list: string[]) => string;
|
|
10
10
|
/**
|
|
11
|
-
* What home greets someone as: their name, capitalised; signed in by
|
|
12
|
-
* its first part, up to a dot ("ana.lima@example.com" is "Ana").
|
|
11
|
+
* What home greets someone as: their first name, capitalised; signed in by
|
|
12
|
+
* email, its first part, up to a dot ("ana.lima@example.com" is "Ana"). An
|
|
13
|
+
* id that isn't a name ("google:1234") greets no one by name.
|
|
13
14
|
*/
|
|
14
15
|
export declare const greeted: (name: string) => string;
|
|
@@ -3,10 +3,13 @@ export const byAgent = (m) => m.turn !== undefined || (m.hops ?? 0) > 0 || m.too
|
|
|
3
3
|
/** Names as a sentence says them: "ana", "ana and bob", "ana, bob and cy". */
|
|
4
4
|
export const names = (list) => list.length < 2 ? list.join("") : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
5
5
|
/**
|
|
6
|
-
* What home greets someone as: their name, capitalised; signed in by
|
|
7
|
-
* its first part, up to a dot ("ana.lima@example.com" is "Ana").
|
|
6
|
+
* What home greets someone as: their first name, capitalised; signed in by
|
|
7
|
+
* email, its first part, up to a dot ("ana.lima@example.com" is "Ana"). An
|
|
8
|
+
* id that isn't a name ("google:1234") greets no one by name.
|
|
8
9
|
*/
|
|
9
10
|
export const greeted = (name) => {
|
|
10
|
-
|
|
11
|
+
if (/^[a-z]+:/.test(name))
|
|
12
|
+
return "";
|
|
13
|
+
const first = name.split("@")[0].split(/[\s._+-]/).find(Boolean) ?? "";
|
|
11
14
|
return first ? first[0].toLocaleUpperCase() + first.slice(1) : "";
|
|
12
15
|
};
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* which is them saying yes. It works once, for minutes. With it, where the
|
|
6
6
|
* computer connects, when the app says (`computers.url`), and where the
|
|
7
7
|
* program is downloaded from, when it says (`downloads`: the release's base,
|
|
8
|
-
* on the app's origin unless absolute, its version and binaries).
|
|
8
|
+
* on the app's origin unless absolute, its version and binaries). On an org,
|
|
9
|
+
* the folder its spaces go in on that computer (`folder`): the org's name,
|
|
10
|
+
* plain, `personal` for one's own (it's named for them).
|
|
9
11
|
*/
|
|
10
12
|
import { defineAction } from "../../../../runtime/mod.js";
|
|
11
13
|
import { mustBeAble } from "../shared/desks.js";
|
|
14
|
+
import { plainName } from "../shared/folder.js";
|
|
12
15
|
import { code, hash } from "../shared/secret.js";
|
|
13
16
|
import { settingsOf } from "../shared/settings.js";
|
|
14
17
|
export const codeAction = defineAction({
|
|
@@ -33,11 +36,14 @@ export const codeAction = defineAction({
|
|
|
33
36
|
in: space,
|
|
34
37
|
});
|
|
35
38
|
const downloads = ctx.resources.downloads;
|
|
39
|
+
const org = ctx.resources.org;
|
|
40
|
+
const folder = org?.kind === "personal" ? "personal" : org?.name && plainName(org.name, "");
|
|
36
41
|
return {
|
|
37
42
|
code: made,
|
|
38
43
|
expires,
|
|
39
44
|
in: space,
|
|
40
45
|
...(url ? { url } : {}),
|
|
46
|
+
...(folder ? { folder } : {}),
|
|
41
47
|
...(downloads ? { downloads: { base: downloads.base, ...downloads.manifest } } : {}),
|
|
42
48
|
};
|
|
43
49
|
},
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A release of the computer program, as `scripts/computer/build.ts` makes it
|
|
3
|
-
* and an app serves it (the computers plugin's `downloads`): `install.sh` and
|
|
4
|
-
* `install.ps1`, a binary per platform, and `manifest.json`, which says the
|
|
5
|
-
* version and each binary's file, size and SHA-256. Plain data and functions
|
|
6
|
-
* only: the build, the app, its web front end and the program all read it.
|
|
7
|
-
*/
|
|
8
1
|
/**
|
|
9
2
|
* The work an app offers a computer a newer release with (`computer.update`;
|
|
10
3
|
* the computers plugin's `computer` workers serve it beside `WORK`'s): the
|
|
@@ -33,9 +26,14 @@ export type Binary = Platform & {
|
|
|
33
26
|
size: number;
|
|
34
27
|
sha256: string;
|
|
35
28
|
};
|
|
36
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* What `manifest.json` says: the program's version, its binaries, and what
|
|
31
|
+
* the program is (`source`: a SHA-256 of what it's built from, its version
|
|
32
|
+
* left out; the same program, the same `source`).
|
|
33
|
+
*/
|
|
37
34
|
export type Manifest = {
|
|
38
35
|
version: string;
|
|
36
|
+
source?: string;
|
|
39
37
|
binaries: Binary[];
|
|
40
38
|
};
|
|
41
39
|
/**
|
|
@@ -74,12 +72,14 @@ export declare const binaryFor: (manifest: Manifest, { os, arch }: Platform) =>
|
|
|
74
72
|
/**
|
|
75
73
|
* The one line that installs the program and connects it with `code`: on
|
|
76
74
|
* macOS and Linux (`sh`), or in Windows' PowerShell. `base` is where the
|
|
77
|
-
* release is (absolute), named only when it isn't the app's own
|
|
75
|
+
* release is (absolute), named only when it isn't the app's own; `folder`,
|
|
76
|
+
* the folder its spaces go in (its org's name), plain, so the line needs no quoting.
|
|
78
77
|
*/
|
|
79
|
-
export declare function installCommand(shell: "sh" | "powershell", { app, code, base }: {
|
|
78
|
+
export declare function installCommand(shell: "sh" | "powershell", { app, code, base, folder }: {
|
|
80
79
|
app: string;
|
|
81
80
|
code: string;
|
|
82
81
|
base: string;
|
|
82
|
+
folder?: string;
|
|
83
83
|
}): string;
|
|
84
84
|
/** Whether version `a` is newer than `b`, as semver orders them (`1.0.0-rc.2` < `1.0.0`). */
|
|
85
85
|
export declare function newer(a: string, b: string): boolean;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* version and each binary's file, size and SHA-256. Plain data and functions
|
|
6
6
|
* only: the build, the app, its web front end and the program all read it.
|
|
7
7
|
*/
|
|
8
|
+
import { plainName } from "../shared/folder.js";
|
|
8
9
|
/**
|
|
9
10
|
* The work an app offers a computer a newer release with (`computer.update`;
|
|
10
11
|
* the computers plugin's `computer` workers serve it beside `WORK`'s): the
|
|
@@ -42,13 +43,15 @@ export const binaryFor = (manifest, { os, arch }) => manifest.binaries.find((bin
|
|
|
42
43
|
/**
|
|
43
44
|
* The one line that installs the program and connects it with `code`: on
|
|
44
45
|
* macOS and Linux (`sh`), or in Windows' PowerShell. `base` is where the
|
|
45
|
-
* release is (absolute), named only when it isn't the app's own
|
|
46
|
+
* release is (absolute), named only when it isn't the app's own; `folder`,
|
|
47
|
+
* the folder its spaces go in (its org's name), plain, so the line needs no quoting.
|
|
46
48
|
*/
|
|
47
|
-
export function installCommand(shell, { app, code, base }) {
|
|
49
|
+
export function installCommand(shell, { app, code, base, folder }) {
|
|
48
50
|
const own = base === new URL(DOWNLOADS, app).href;
|
|
51
|
+
const named = folder ? plainName(folder, "") : "";
|
|
49
52
|
return shell === "sh"
|
|
50
|
-
? `curl -fsSL ${base}install.sh | sh -s -- ${app} --code ${code}${own ? "" : ` --from ${base}`}`
|
|
51
|
-
: `& ([scriptblock]::Create((irm ${base}install.ps1))) ${app} -Code ${code}${own ? "" : ` -From ${base}`}`;
|
|
53
|
+
? `curl -fsSL ${base}install.sh | sh -s -- ${app} --code ${code}${own ? "" : ` --from ${base}`}${named ? ` --folder ${named}` : ""}`
|
|
54
|
+
: `& ([scriptblock]::Create((irm ${base}install.ps1))) ${app} -Code ${code}${own ? "" : ` -From ${base}`}${named ? ` -Folder ${named}` : ""}`;
|
|
52
55
|
}
|
|
53
56
|
/** A version's parts: its numbers, and its pre-release's (`1.0.0-rc.1`: rc, 1). */
|
|
54
57
|
const parts = (version) => {
|
|
@@ -71,19 +71,19 @@ export const commands = {
|
|
|
71
71
|
const [what, n, ...name] = rest.trim().split(/\s+/);
|
|
72
72
|
const { space, all } = await listed(term);
|
|
73
73
|
if (what === "connect") {
|
|
74
|
-
const { code, expires, url, downloads } = await term.runtime.call("computer.code", space ? { in: space } : {});
|
|
74
|
+
const { code, expires, url, folder, downloads } = await term.runtime.call("computer.code", space ? { in: space } : {});
|
|
75
75
|
const minutes = Math.round((Date.parse(expires) - Date.now()) / 60_000);
|
|
76
76
|
const app = url ?? "<app url>";
|
|
77
77
|
// From the app's release when it has one; else from a copy of the app's code.
|
|
78
78
|
const base = downloads && url ? new URL(downloads.base, url).href : undefined;
|
|
79
79
|
return await term.print(`On the computer this space's agents should use, run:\n\n` +
|
|
80
80
|
(base
|
|
81
|
-
? ` ${installCommand("sh", { app, code, base })}\n\n` +
|
|
81
|
+
? ` ${installCommand("sh", { app, code, base, folder })}\n\n` +
|
|
82
82
|
`On Windows, in PowerShell:\n\n` +
|
|
83
|
-
` ${installCommand("powershell", { app, code, base })}\n\n` +
|
|
83
|
+
` ${installCommand("powershell", { app, code, base, folder })}\n\n` +
|
|
84
84
|
dim("It installs the computer program, connects it, and keeps it running (from " +
|
|
85
85
|
"login on). Their folders go under ~/Copilotz there. ")
|
|
86
|
-
: ` deno task computer connect ${app} --code ${code}\n\n` +
|
|
86
|
+
: ` deno task computer connect ${app} --code ${code}${folder ? ` --folder ${folder}` : ""}\n\n` +
|
|
87
87
|
dim("Their folders go under ~/Copilotz there (--root puts them elsewhere). ")) +
|
|
88
88
|
dim(`The code works once, for ${minutes} minutes.`));
|
|
89
89
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type Plugin } from "../../../../runtime/mod.js";
|
|
2
2
|
import { type Manifest } from "../program/release.js";
|
|
3
|
+
/** Where Copilotz's releases are: each one's computer program, at `v<version>/`. */
|
|
4
|
+
export declare const RELEASES = "https://github.com/copilotzhq/copilotz/releases/download/";
|
|
5
|
+
/** This Copilotz's release of the computer program (the default `computerDownloads`). */
|
|
6
|
+
export declare const releaseOf: (version?: string) => string;
|
|
3
7
|
/**
|
|
4
8
|
* What the resource says: where the release is (absolute, or on the app's
|
|
5
9
|
* origin), and what's in it. The plugin's Actions read it by type alone, so
|
|
@@ -9,8 +13,8 @@ export type Downloads = {
|
|
|
9
13
|
base: string;
|
|
10
14
|
manifest: Manifest;
|
|
11
15
|
};
|
|
12
|
-
/** The app's downloads (see above),
|
|
13
|
-
export declare function computerDownloads(from
|
|
16
|
+
/** The app's downloads (see above): this Copilotz's release, a release folder of its own, or at a URL. */
|
|
17
|
+
export declare function computerDownloads(from?: {
|
|
14
18
|
dir: string;
|
|
15
19
|
} | {
|
|
16
20
|
url: string;
|
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
* connect flow shows the install command and the binaries from it; a
|
|
6
6
|
* computer older than it updates itself to it (`computer.ready`).
|
|
7
7
|
*
|
|
8
|
-
* `computerDownloads` gives an app both, as a plugin:
|
|
8
|
+
* `computerDownloads` gives an app both, as a plugin: by default this
|
|
9
|
+
* Copilotz's own release on GitHub (`v<version>`: every release has the
|
|
10
|
+
* program, built again only when it changed); or a release folder of its
|
|
9
11
|
* own, served from its origin at `/computer/download/` (a `routes` entry of
|
|
10
|
-
* the web host: only the release's files, as they are)
|
|
11
|
-
*
|
|
12
|
+
* the web host: only the release's files, as they are); or one at a URL (a
|
|
13
|
+
* bucket, another release). It reads the manifest once, as it starts.
|
|
12
14
|
*
|
|
13
15
|
* ```ts
|
|
14
|
-
* plugins: [...plugins, await computerDownloads(
|
|
15
|
-
* plugins: [...plugins, await computerDownloads({
|
|
16
|
+
* plugins: [...plugins, await computerDownloads()]
|
|
17
|
+
* plugins: [...plugins, await computerDownloads({ dir: "dist/computer/1.0.0-rc.2" })]
|
|
18
|
+
* plugins: [...plugins, await computerDownloads({ url: "https://downloads.example.com/computer/" })]
|
|
16
19
|
* ```
|
|
17
20
|
*/
|
|
18
21
|
import { createReadStream } from "node:fs";
|
|
@@ -21,6 +24,11 @@ import { join, resolve } from "node:path";
|
|
|
21
24
|
import { Readable } from "node:stream";
|
|
22
25
|
import { definePlugin } from "../../../../runtime/mod.js";
|
|
23
26
|
import { DOWNLOADS } from "../program/release.js";
|
|
27
|
+
import { VERSION } from "../program/version.js";
|
|
28
|
+
/** Where Copilotz's releases are: each one's computer program, at `v<version>/`. */
|
|
29
|
+
export const RELEASES = "https://github.com/copilotzhq/copilotz/releases/download/";
|
|
30
|
+
/** This Copilotz's release of the computer program (the default `computerDownloads`). */
|
|
31
|
+
export const releaseOf = (version = VERSION) => `${RELEASES}v${version}/`;
|
|
24
32
|
/** A manifest, checked for what everything reading it relies on. */
|
|
25
33
|
function checked(value, from) {
|
|
26
34
|
const manifest = value;
|
|
@@ -71,8 +79,8 @@ function serveRelease(dir, manifest) {
|
|
|
71
79
|
return new Response(body, { headers });
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
|
-
/** The app's downloads (see above),
|
|
75
|
-
export async function computerDownloads(from) {
|
|
82
|
+
/** The app's downloads (see above): this Copilotz's release, a release folder of its own, or at a URL. */
|
|
83
|
+
export async function computerDownloads(from = { url: releaseOf() }) {
|
|
76
84
|
if ("url" in from) {
|
|
77
85
|
const base = from.url.endsWith("/") ? from.url : `${from.url}/`;
|
|
78
86
|
const response = await fetch(new URL("manifest.json", base));
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
/** A
|
|
1
|
+
/** A name as a folder's: plain (one segment, lowercase), or `fallback` when nothing's left. */
|
|
2
|
+
export declare function plainName(name: string, fallback?: string): string;
|
|
3
|
+
/** A space's folder name on a computer: its name, plain, kept apart from the others'. */
|
|
2
4
|
export declare function folderFor(name: string, taken: string[]): string;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
/** A
|
|
1
|
+
/** A name as a folder's: plain (one segment, lowercase), or `fallback` when nothing's left. */
|
|
2
|
+
export function plainName(name, fallback = "space") {
|
|
3
|
+
return name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase()
|
|
4
|
+
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || fallback;
|
|
5
|
+
}
|
|
6
|
+
/** A space's folder name on a computer: its name, plain, kept apart from the others'. */
|
|
2
7
|
export function folderFor(name, taken) {
|
|
3
|
-
const base = name
|
|
4
|
-
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "space";
|
|
8
|
+
const base = plainName(name);
|
|
5
9
|
let folder = base;
|
|
6
10
|
for (let n = 2; taken.includes(folder); n++)
|
|
7
11
|
folder = `${base}-${n}`;
|
|
@@ -122,7 +122,7 @@ export function ConnectDialog({ open, onClose, call, computers, here, where, not
|
|
|
122
122
|
? (_jsxs(_Fragment, { children: [_jsx("span", { children: "This code has run out." }), _jsx(Button, { size: "sm", icon: "refresh", onClick: () => void fresh(), children: "New code" })] }))
|
|
123
123
|
: (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "lock", size: 13 }), _jsxs("span", { children: ["The code works once, for", " ", _jsx("b", { className: "connect-left", children: clock(left) }), " more"] })] })) }))
|
|
124
124
|
: null, release && code
|
|
125
|
-
? (_jsx(Direct, { release: release, system: system, mine: system === visitorSystem(), arch: arch, app: app, code: code.code, spent: expired }))
|
|
125
|
+
? (_jsx(Direct, { release: release, system: system, mine: system === visitorSystem(), arch: arch, app: app, code: code.code, folder: code.folder, spent: expired }))
|
|
126
126
|
: null] })] }), _jsxs("li", { children: [_jsx("span", { className: "connect-step-number", "aria-hidden": "true", children: "2" }), _jsx("div", { className: "connect-step", children: _jsx("p", { children: release
|
|
127
127
|
? (_jsxs(_Fragment, { children: ["It connects, then keeps running in the background, and starts when you log in. Each space gets a folder in", " ", _jsx("code", { children: system === "win32" ? "%USERPROFILE%\\Copilotz" : "~/Copilotz" }), " ", "on it."] }))
|
|
128
128
|
: (_jsxs(_Fragment, { children: ["Leave it running. Each space gets a folder in ", _jsx("code", { children: "~/Copilotz" }), " ", "on that computer (", _jsx("code", { children: "--root" }), " puts them elsewhere), and next time", " ", _jsx("code", { children: "deno task computer" }), " starts it again."] })) }) })] }), code && !expired
|
|
@@ -135,16 +135,23 @@ export function ConnectDialog({ open, onClose, call, computers, here, where, not
|
|
|
135
135
|
function Command({ code, app, system, spent }) {
|
|
136
136
|
const release = code.downloads;
|
|
137
137
|
if (!release) {
|
|
138
|
-
const
|
|
138
|
+
const folder = code.folder ? ` --folder ${code.folder}` : "";
|
|
139
|
+
const command = `deno task computer connect ${app} --code ${code.code}${folder}`;
|
|
139
140
|
return (_jsx(CodeBlock, { className: "connect-command", label: "The command to run", code: [
|
|
140
141
|
{ text: "$ ", tone: "muted" },
|
|
141
142
|
{ text: `deno task computer connect ${app} \\\n --code ` },
|
|
142
143
|
{ text: code.code, tone: spent ? "muted" : "accent" },
|
|
144
|
+
...(folder ? [{ text: folder }] : []),
|
|
143
145
|
], copy: spent ? false : command }));
|
|
144
146
|
}
|
|
145
147
|
const base = within(release.base, app);
|
|
146
148
|
const windows = system === "win32";
|
|
147
|
-
const command = installCommand(windows ? "powershell" : "sh", {
|
|
149
|
+
const command = installCommand(windows ? "powershell" : "sh", {
|
|
150
|
+
app,
|
|
151
|
+
code: code.code,
|
|
152
|
+
base,
|
|
153
|
+
folder: code.folder,
|
|
154
|
+
});
|
|
148
155
|
return (_jsx(CodeBlock, { className: "connect-command", label: "The command that installs it", code: parts(command, code.code, spent, windows ? "PS> " : "$ "), copy: spent ? false : command }));
|
|
149
156
|
}
|
|
150
157
|
/**
|
|
@@ -152,7 +159,7 @@ function Command({ code, app, system, spent }) {
|
|
|
152
159
|
* when it's theirs and their architecture is known), each with its size and
|
|
153
160
|
* SHA-256, and how to run one; behind a disclosure.
|
|
154
161
|
*/
|
|
155
|
-
function Direct({ release, system, mine, arch, app, code, spent }) {
|
|
162
|
+
function Direct({ release, system, mine, arch, app, code, folder, spent }) {
|
|
156
163
|
const base = within(release.base, app);
|
|
157
164
|
const binaries = release.binaries.filter((b) => b.os === system)
|
|
158
165
|
.sort((a, b) => Number(mine && b.arch === arch) - Number(mine && a.arch === arch));
|
|
@@ -165,9 +172,10 @@ function Direct({ release, system, mine, arch, app, code, spent }) {
|
|
|
165
172
|
: undefined;
|
|
166
173
|
const windows = system === "win32";
|
|
167
174
|
const path = windows ? `$HOME\\Downloads\\${first.file}` : `~/Downloads/${first.file}`;
|
|
175
|
+
const named = folder ? ` --folder ${folder}` : "";
|
|
168
176
|
const run = windows
|
|
169
|
-
? `& "${path}" install ${app} --code ${code}`
|
|
170
|
-
: `chmod +x ${path} && ${path} install ${app} --code ${code}`;
|
|
177
|
+
? `& "${path}" install ${app} --code ${code}${named}`
|
|
178
|
+
: `chmod +x ${path} && ${path} install ${app} --code ${code}${named}`;
|
|
171
179
|
return (_jsxs("details", { className: "connect-direct", children: [_jsxs("summary", { children: [_jsx(Icon, { name: "chevron", size: 14, className: "connect-direct-chevron" }), "Download it instead", _jsxs("span", { className: "connect-direct-version", children: ["v", release.version] })] }), _jsxs("div", { className: "connect-direct-body", children: [_jsx("ul", { className: "connect-binaries", "aria-label": `For ${OS_NAMES[system]}`, children: binaries.map((b) => (_jsxs("li", { className: "connect-binary", children: [_jsxs("div", { className: "connect-binary-text", children: [_jsxs("span", { className: "connect-binary-name", children: [archName(b.os, b.arch), yours === b ? _jsx("span", { className: "connect-binary-yours", children: "this computer" }) : null] }), _jsxs("span", { className: "connect-binary-facts", children: [_jsx("code", { children: b.file }), " \u00B7 ", bytes(b.size)] }), _jsxs("span", { className: "connect-binary-sha", title: b.sha256, children: ["SHA-256 ", _jsxs("code", { children: [b.sha256.slice(0, 12), "\u2026", b.sha256.slice(-8)] }), _jsx(CopyButton, { text: b.sha256, label: "Copy SHA-256" })] })] }), _jsx(Button, { size: "sm", icon: "download", href: `${base}${b.file}`, download: b.file, children: "Download" })] }, b.file))) }), _jsx("p", { children: "Then, where you saved it, run it with your code:" }), _jsx(CodeBlock, { className: "connect-command", label: "How to run the download", code: parts(run, code, spent, windows ? "PS> " : "$ "), copy: spent ? false : run }), _jsxs("p", { className: "connect-unsigned", children: [_jsx(Icon, { name: "alert", size: 13 }), _jsx("span", { children: windows
|
|
172
180
|
? "The program isn't signed yet, so Windows may say it doesn't recognize it (More " +
|
|
173
181
|
"info, then Run anyway). The command above doesn't need that."
|
|
@@ -21,13 +21,15 @@ import { type PaletteSource } from "../shared/palette.js";
|
|
|
21
21
|
import type { UserMenuEntry } from "../shared/menu.js";
|
|
22
22
|
import { type Offered } from "./panel.js";
|
|
23
23
|
type Call = <R = unknown>(action: string, input?: unknown) => Promise<R>;
|
|
24
|
-
export declare function Frame({ path, brand, viewer, sidebar, menu, call, panel, palette, children }: {
|
|
24
|
+
export declare function Frame({ path, brand, viewer, label, sidebar, menu, call, panel, palette, children }: {
|
|
25
25
|
/** Where the page is: going elsewhere closes the drawer, and the panel's sheet. */
|
|
26
26
|
path: string;
|
|
27
27
|
/** The app's name, which its logo says (or shows). */
|
|
28
28
|
brand: string;
|
|
29
29
|
/** Who's looking: the panel is kept as they left it. */
|
|
30
30
|
viewer: string;
|
|
31
|
+
/** What the viewer is shown as, when not their id (their name). */
|
|
32
|
+
label?: string;
|
|
31
33
|
sidebar: ReactNode;
|
|
32
34
|
/** What front ends put in the person's menu (the `userMenu` resource). */
|
|
33
35
|
menu: UserMenuEntry[];
|
|
@@ -36,7 +36,7 @@ const phone = {
|
|
|
36
36
|
},
|
|
37
37
|
matches: () => matchMedia(PHONE).matches,
|
|
38
38
|
};
|
|
39
|
-
export function Frame({ path, brand, viewer, sidebar, menu, call, panel, palette, children }) {
|
|
39
|
+
export function Frame({ path, brand, viewer, label, sidebar, menu, call, panel, palette, children }) {
|
|
40
40
|
const narrow = useSyncExternalStore(phone.subscribe, phone.matches);
|
|
41
41
|
const [open, setOpen] = useState(false);
|
|
42
42
|
useEffect(() => setOpen(false), [path, narrow]);
|
|
@@ -104,7 +104,7 @@ export function Frame({ path, brand, viewer, sidebar, menu, call, panel, palette
|
|
|
104
104
|
: (_jsx("a", { className: "copilotz-brand", href: "/", children: _jsx(Logo, { variant: "full", name: brand, size: 32 }) })), narrow
|
|
105
105
|
? _jsx(IconButton, { icon: "close", label: "Close sidebar", onClick: () => setOpen(false) })
|
|
106
106
|
: null] }), _jsx("div", { className: "copilotz-find", children: _jsx(NavItem, { quiet: true, icon: "search", onClick: find, end: narrow ? null : _jsx(Kbd, { children: shortcutOf(mac) }), title: `Search spaces and chats, or run a command (${shortcutOf(mac)})`, children: "Search" }) }), _jsx("nav", { className: "copilotz-sections", "aria-label": "Spaces and chats", children: sidebar }), viewer
|
|
107
|
-
? (_jsx(Me, { name: viewer, email: orgs?.email, menu: menu, ...(orgs ? { orgs: { here: !!here, standing, staff: orgs.staff } } : {}) }))
|
|
107
|
+
? (_jsx(Me, { name: label ?? viewer, email: orgs?.email, menu: menu, ...(orgs ? { orgs: { here: !!here, standing, staff: orgs.staff } } : {}) }))
|
|
108
108
|
: null] }));
|
|
109
109
|
return (_jsxs("div", { className: "copilotz-shell", "data-staff": staff || undefined, children: [_jsx("style", { href: "web-shell", precedence: "copilotz", children: STYLE }), staff
|
|
110
110
|
? (_jsx("div", { className: "copilotz-staff-line", role: "note", children: _jsx("span", { className: "copilotz-sr", children: "You're in this org as platform staff: what you do is on its record." }) }))
|
|
@@ -19,7 +19,8 @@ export const shellView = (where, ctx) => {
|
|
|
19
19
|
const open = kind ? ctx.show(kind, id ? { _id: id } : {}, asked) : ctx.show("home", {}, asked);
|
|
20
20
|
const panel = ctx.resources.panel;
|
|
21
21
|
const viewer = ctx.viewer?.name ?? "";
|
|
22
|
-
|
|
22
|
+
const label = ctx.viewer?.label;
|
|
23
|
+
return (_jsx(Frame, { path: where.path + (where.space ?? ""), brand: app, viewer: viewer, ...(label ? { label } : {}), palette: {
|
|
23
24
|
sources: sourcesOf(ctx.resources.palette),
|
|
24
25
|
hints: {
|
|
25
26
|
...hints,
|
package/runtime/define.d.ts
CHANGED
|
@@ -169,10 +169,15 @@ export type Merged = {
|
|
|
169
169
|
export type Views = Record<string, Record<string, ViewDef>>;
|
|
170
170
|
/** A View: an item, for a medium. Its context says what showing another View returns there. */
|
|
171
171
|
export type ViewDef = (item: any, ctx: ViewContext<any>) => unknown;
|
|
172
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Who looks: a View renders relative to it (your own messages, your bubbles).
|
|
174
|
+
* `name` is who they are (a person's id); `label` what they're shown as, when
|
|
175
|
+
* it isn't that (a person's own name, as they signed in).
|
|
176
|
+
*/
|
|
173
177
|
export type Viewer = {
|
|
174
178
|
name: string;
|
|
175
179
|
kind?: string;
|
|
180
|
+
label?: string;
|
|
176
181
|
};
|
|
177
182
|
export type RenderOptions = {
|
|
178
183
|
viewer?: Viewer;
|