@fungi.computer/nacre 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/app-directory.d.ts +600 -0
- package/dist/app-directory.js +323 -0
- package/dist/browser-actions.d.ts +7 -0
- package/dist/browser-actions.js +56 -0
- package/dist/command-host.d.ts +140 -0
- package/dist/command-host.js +532 -0
- package/dist/commands.d.ts +25 -0
- package/dist/commands.js +78 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/keys.d.ts +47 -0
- package/dist/keys.js +136 -0
- package/dist/shell-module.d.ts +98 -0
- package/dist/shell-module.js +90 -0
- package/dist/window-manager.d.ts +159 -0
- package/dist/window-manager.js +796 -0
- package/dist/workbench-contract.d.ts +70 -0
- package/dist/workbench-contract.js +1 -0
- package/dist/workbench-directory.d.ts +343 -0
- package/dist/workbench-directory.js +65 -0
- package/dist/workbench-host-types.d.ts +114 -0
- package/dist/workbench-host-types.js +1 -0
- package/dist/workbench-host.d.ts +34 -0
- package/dist/workbench-host.js +356 -0
- package/dist/workbench-model.d.ts +223 -0
- package/dist/workbench-model.js +93 -0
- package/dist/workbench.d.ts +4 -0
- package/dist/workbench.js +2 -0
- package/docs/api/README.md +21 -0
- package/docs/api/app-directory.md +587 -0
- package/docs/api/command-host.md +225 -0
- package/docs/api/commands.md +199 -0
- package/docs/api/index.md +1860 -0
- package/docs/api/keys.md +183 -0
- package/docs/api/shell-module.md +233 -0
- package/docs/api/window-manager.md +269 -0
- package/docs/api/workbench-directory.md +141 -0
- package/docs/api/workbench.md +201 -0
- package/package.json +98 -3
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { AppError, parseAppStatusAction } from "@fungi.computer/agar";
|
|
3
|
+
import { parseJsonValue } from "@fungi.computer/whistle/wire";
|
|
4
|
+
import { desktopSettingsHandlerSchema, } from "./workbench-model.js";
|
|
5
|
+
import { createAppSurfaceCommandHost, } from "./command-host.js";
|
|
6
|
+
/** A dashboard may keep this many child Apps mounted at once. */
|
|
7
|
+
const maxChildren = 32;
|
|
8
|
+
const nonBlank = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(2_048)));
|
|
9
|
+
const visibilitySchema = Schema.Literals(["hidden", "visible"]);
|
|
10
|
+
// A dashboard presents a child's commands to people; it never acts as the
|
|
11
|
+
// child's Agent.
|
|
12
|
+
const commandInvocationSchema = Schema.Struct({
|
|
13
|
+
commandId: nonBlank,
|
|
14
|
+
origin: Schema.Literals([
|
|
15
|
+
"keyboard",
|
|
16
|
+
"palette",
|
|
17
|
+
"menu",
|
|
18
|
+
"browser",
|
|
19
|
+
"programmatic",
|
|
20
|
+
"slash",
|
|
21
|
+
"terminal",
|
|
22
|
+
]),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Forward a child's Release-validated commands and reserved chords to the
|
|
26
|
+
* dashboard that presents them. Validation stays here, against the child's
|
|
27
|
+
* own Release declaration, so the dashboard can never widen them.
|
|
28
|
+
*/
|
|
29
|
+
function forwardCommands(log, validated, events, current) {
|
|
30
|
+
const present = (commands) => events.onCommands?.(commands) ?? Promise.resolve();
|
|
31
|
+
const handlers = {
|
|
32
|
+
registerCommands: async (input) => {
|
|
33
|
+
const contribution = await validated.handlers.registerCommands(input);
|
|
34
|
+
current.invoke = input.invoke;
|
|
35
|
+
await present(input.commands);
|
|
36
|
+
return {
|
|
37
|
+
update: (commands) => {
|
|
38
|
+
contribution.update(commands);
|
|
39
|
+
void present(commands).catch((cause) => {
|
|
40
|
+
log("team_workbench_commands_failed", {
|
|
41
|
+
stage: "update",
|
|
42
|
+
cause,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
dispose: () => {
|
|
47
|
+
contribution.dispose();
|
|
48
|
+
if (current.invoke === input.invoke)
|
|
49
|
+
current.invoke = undefined;
|
|
50
|
+
void present([]).catch((cause) => {
|
|
51
|
+
log("team_workbench_commands_failed", {
|
|
52
|
+
stage: "dispose",
|
|
53
|
+
cause,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
invokeReservedChord: async (input) => {
|
|
60
|
+
if (events.onReservedChord === undefined)
|
|
61
|
+
throw new Error("App reserved chord is unavailable");
|
|
62
|
+
await events.onReservedChord(input);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
agentCommands: validated.agentCommands,
|
|
67
|
+
handlers: Object.freeze(handlers),
|
|
68
|
+
dispose: validated.dispose,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One mounted child App, proxying to its real host-owned mount. The shell
|
|
73
|
+
* holds this bounded handle; the host's lease machinery stays behind it.
|
|
74
|
+
*/
|
|
75
|
+
class MountedChildHandle {
|
|
76
|
+
surface;
|
|
77
|
+
commands;
|
|
78
|
+
reconfigureCurrent;
|
|
79
|
+
release;
|
|
80
|
+
#disposed = false;
|
|
81
|
+
constructor(surface, commands, reconfigureCurrent, release) {
|
|
82
|
+
this.surface = surface;
|
|
83
|
+
this.commands = commands;
|
|
84
|
+
this.reconfigureCurrent = reconfigureCurrent;
|
|
85
|
+
this.release = release;
|
|
86
|
+
}
|
|
87
|
+
async setVisibility(visibility) {
|
|
88
|
+
if (this.#disposed)
|
|
89
|
+
return;
|
|
90
|
+
await this.surface.setVisibility(Schema.decodeUnknownSync(visibilitySchema)(visibility));
|
|
91
|
+
}
|
|
92
|
+
async reconfigure(launch) {
|
|
93
|
+
if (this.#disposed)
|
|
94
|
+
return;
|
|
95
|
+
await this.reconfigureCurrent(launch);
|
|
96
|
+
}
|
|
97
|
+
async requestClose() {
|
|
98
|
+
if (this.#disposed)
|
|
99
|
+
return true;
|
|
100
|
+
return this.surface.requestClose();
|
|
101
|
+
}
|
|
102
|
+
async requestBack() {
|
|
103
|
+
if (this.#disposed)
|
|
104
|
+
return false;
|
|
105
|
+
return this.surface.requestBack();
|
|
106
|
+
}
|
|
107
|
+
async activateStatus(action) {
|
|
108
|
+
if (this.#disposed)
|
|
109
|
+
return false;
|
|
110
|
+
// The shell relays the person's click; parse it at the host's edge.
|
|
111
|
+
return this.surface.activateStatus(parseAppStatusAction(action));
|
|
112
|
+
}
|
|
113
|
+
async activateNavigation(activation) {
|
|
114
|
+
if (this.#disposed)
|
|
115
|
+
return false;
|
|
116
|
+
return this.surface.activateNavigation(activation);
|
|
117
|
+
}
|
|
118
|
+
async invokeCommand(commandId, input) {
|
|
119
|
+
const invoke = this.commands.invoke;
|
|
120
|
+
if (this.#disposed || invoke === undefined)
|
|
121
|
+
throw new AppError("That App command is unavailable", {
|
|
122
|
+
code: "CAPABILITY_DENIED",
|
|
123
|
+
retryable: false,
|
|
124
|
+
});
|
|
125
|
+
const parsed = Schema.decodeUnknownSync(commandInvocationSchema)({
|
|
126
|
+
commandId,
|
|
127
|
+
origin: input.origin,
|
|
128
|
+
});
|
|
129
|
+
return invoke(parsed.commandId, input.arguments === undefined
|
|
130
|
+
? { origin: parsed.origin }
|
|
131
|
+
: { arguments: parseJsonValue(input.arguments), origin: parsed.origin });
|
|
132
|
+
}
|
|
133
|
+
async dispose() {
|
|
134
|
+
if (this.#disposed)
|
|
135
|
+
return;
|
|
136
|
+
this.#disposed = true;
|
|
137
|
+
await this.release();
|
|
138
|
+
}
|
|
139
|
+
[Symbol.dispose]() {
|
|
140
|
+
void this.dispose();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
class DesktopSettingsRegistration {
|
|
144
|
+
detach;
|
|
145
|
+
constructor(detach) {
|
|
146
|
+
this.detach = detach;
|
|
147
|
+
}
|
|
148
|
+
close() {
|
|
149
|
+
this.detach();
|
|
150
|
+
return Promise.resolve();
|
|
151
|
+
}
|
|
152
|
+
[Symbol.dispose]() {
|
|
153
|
+
this.detach();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Child-window, command and callback lifetime for the trusted shell the host
|
|
158
|
+
* composes into its own document. Each child mounts inside the window slot
|
|
159
|
+
* the shell hands it, so the browser moves, clips and stacks it with that
|
|
160
|
+
* window; the host never positions a child itself.
|
|
161
|
+
*/
|
|
162
|
+
export class WorkbenchHost {
|
|
163
|
+
#hostWindow;
|
|
164
|
+
#options;
|
|
165
|
+
#commands = createAppSurfaceCommandHost();
|
|
166
|
+
#children = new Set();
|
|
167
|
+
#releases = [];
|
|
168
|
+
#desktopSettingsClose;
|
|
169
|
+
#settingsClose;
|
|
170
|
+
#finishSettings;
|
|
171
|
+
constructor(options) {
|
|
172
|
+
this.#hostWindow = options.hostWindow;
|
|
173
|
+
this.#options = options;
|
|
174
|
+
}
|
|
175
|
+
team() {
|
|
176
|
+
return this.#options.services.team();
|
|
177
|
+
}
|
|
178
|
+
computers(input) {
|
|
179
|
+
return this.#options.services.computers(input);
|
|
180
|
+
}
|
|
181
|
+
directory() {
|
|
182
|
+
return this.#options.services.directory();
|
|
183
|
+
}
|
|
184
|
+
requestGill(input) {
|
|
185
|
+
return this.#options.services.requestGill(input);
|
|
186
|
+
}
|
|
187
|
+
watchGillChanges(...input) {
|
|
188
|
+
return this.#options.services.watchGillChanges(...input);
|
|
189
|
+
}
|
|
190
|
+
async platformDestinations() {
|
|
191
|
+
return (await this.#options.platform.destinations()).map(({ id, label }) => ({ id, label }));
|
|
192
|
+
}
|
|
193
|
+
async openPlatformDestination(id) {
|
|
194
|
+
const parsed = Schema.decodeUnknownSync(nonBlank)(id);
|
|
195
|
+
const offered = await this.#options.platform.destinations();
|
|
196
|
+
if (!offered.some((destination) => destination.id === parsed))
|
|
197
|
+
throw new AppError("That destination is unavailable", {
|
|
198
|
+
code: "INVALID_ARGUMENT",
|
|
199
|
+
retryable: false,
|
|
200
|
+
});
|
|
201
|
+
this.#options.platform.open(parsed);
|
|
202
|
+
}
|
|
203
|
+
/** Describe the actual controls without offering unsupported Settings. */
|
|
204
|
+
settingsSections() {
|
|
205
|
+
const controls = this.#options.platform.controls;
|
|
206
|
+
return Promise.resolve(controls.sections?.slice() ?? null);
|
|
207
|
+
}
|
|
208
|
+
takeAppOpenRequest() {
|
|
209
|
+
return Promise.resolve(this.#options.platform.takeAppOpenRequest());
|
|
210
|
+
}
|
|
211
|
+
registerDesktopSettings(handler) {
|
|
212
|
+
return Promise.resolve().then(() => {
|
|
213
|
+
let parsed;
|
|
214
|
+
try {
|
|
215
|
+
parsed = desktopSettingsHandlerSchema.parse(handler);
|
|
216
|
+
}
|
|
217
|
+
catch (cause) {
|
|
218
|
+
throw new AppError("Desktop settings registration is invalid", {
|
|
219
|
+
code: "INVALID_ARGUMENT",
|
|
220
|
+
retryable: false,
|
|
221
|
+
cause,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
this.#desktopSettingsClose?.();
|
|
225
|
+
const detach = this.#options.platform.controls.attachDesktopSettings(parsed);
|
|
226
|
+
let closed = false;
|
|
227
|
+
const close = () => {
|
|
228
|
+
if (closed)
|
|
229
|
+
return;
|
|
230
|
+
closed = true;
|
|
231
|
+
detach();
|
|
232
|
+
if (this.#desktopSettingsClose === close)
|
|
233
|
+
this.#desktopSettingsClose = undefined;
|
|
234
|
+
};
|
|
235
|
+
this.#desktopSettingsClose = close;
|
|
236
|
+
return new DesktopSettingsRegistration(close);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async openSettings(location) {
|
|
240
|
+
const parsed = this.#options.platform.controls.parseSettings(location);
|
|
241
|
+
const controls = this.#options.platform.controls;
|
|
242
|
+
if (this.#settingsClose !== undefined) {
|
|
243
|
+
const closed = this.#settingsClose;
|
|
244
|
+
controls.openSettings(parsed, { restoreFocus: false });
|
|
245
|
+
return closed;
|
|
246
|
+
}
|
|
247
|
+
const closed = new Promise((resolve) => {
|
|
248
|
+
const unsubscribe = controls.subscribeSettingsClosed(() => this.#finishSettings?.());
|
|
249
|
+
this.#finishSettings = () => {
|
|
250
|
+
unsubscribe();
|
|
251
|
+
this.#settingsClose = undefined;
|
|
252
|
+
this.#finishSettings = undefined;
|
|
253
|
+
resolve();
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
this.#settingsClose = closed;
|
|
257
|
+
// The shell restores its own invoker once Settings close; returning
|
|
258
|
+
// focus here would race that restoration.
|
|
259
|
+
controls.openSettings(parsed, { restoreFocus: false });
|
|
260
|
+
return closed;
|
|
261
|
+
}
|
|
262
|
+
memberAvatar() {
|
|
263
|
+
return this.#options.services.memberAvatar();
|
|
264
|
+
}
|
|
265
|
+
watchSettingsChanges(onChanged) {
|
|
266
|
+
const unsubscribe = this.#options.platform.controls.subscribeChanges(() => {
|
|
267
|
+
void onChanged().catch((cause) => {
|
|
268
|
+
this.#options.log("team_workbench_settings_notification_failed", {
|
|
269
|
+
cause,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
this.#releases.push(unsubscribe);
|
|
274
|
+
return Promise.resolve();
|
|
275
|
+
}
|
|
276
|
+
/** The shell unmounted: stop notifying its callbacks. */
|
|
277
|
+
[Symbol.dispose]() {
|
|
278
|
+
this.#finishSettings?.();
|
|
279
|
+
this.#desktopSettingsClose?.();
|
|
280
|
+
this.#options.services.dispose();
|
|
281
|
+
for (const release of this.#releases.splice(0))
|
|
282
|
+
release();
|
|
283
|
+
}
|
|
284
|
+
async mount(container, input, events, signal) {
|
|
285
|
+
const request = this.#options.resolveLaunch(input);
|
|
286
|
+
const mountId = request.launch.windowId;
|
|
287
|
+
if (this.#children.has(mountId))
|
|
288
|
+
throw new AppError("That App window is already open", {
|
|
289
|
+
code: "INVALID_ARGUMENT",
|
|
290
|
+
retryable: false,
|
|
291
|
+
});
|
|
292
|
+
if (this.#children.size >= maxChildren)
|
|
293
|
+
throw new AppError("Too many Apps are open", {
|
|
294
|
+
code: "INVALID_ARGUMENT",
|
|
295
|
+
retryable: false,
|
|
296
|
+
});
|
|
297
|
+
this.#children.add(mountId);
|
|
298
|
+
let prepared;
|
|
299
|
+
let commandMount;
|
|
300
|
+
const childCommands = { invoke: undefined };
|
|
301
|
+
const release = async () => {
|
|
302
|
+
try {
|
|
303
|
+
commandMount?.dispose();
|
|
304
|
+
if (prepared)
|
|
305
|
+
await prepared.dispose();
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
this.#children.delete(mountId);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
try {
|
|
312
|
+
signal.throwIfAborted();
|
|
313
|
+
prepared = await this.#options.prepareChild(request.launch, signal, () => events.onDeliveryPending?.());
|
|
314
|
+
signal.throwIfAborted();
|
|
315
|
+
commandMount = forwardCommands(this.#options.log, this.#commands.mount(mountId, prepared.commands), events, childCommands);
|
|
316
|
+
const surface = await prepared.mount({
|
|
317
|
+
container,
|
|
318
|
+
hostWindow: this.#hostWindow,
|
|
319
|
+
...(request.title === undefined ? undefined : { title: request.title }),
|
|
320
|
+
commands: commandMount,
|
|
321
|
+
events,
|
|
322
|
+
onDesktopSettings: (surfaceId) => this.#options.services.suggestDesktopSettings(surfaceId),
|
|
323
|
+
onPermissionRequest: (permission, context) => this.#options.platform.controls.requestAppAccess({
|
|
324
|
+
surfaceId: request.launch.surfaceId,
|
|
325
|
+
title: request.title ?? request.launch.appId,
|
|
326
|
+
computerId: context.computerId,
|
|
327
|
+
}, permission),
|
|
328
|
+
});
|
|
329
|
+
const reconfigureCurrent = async (next) => {
|
|
330
|
+
const nextRequest = this.#options.resolveLaunch(next);
|
|
331
|
+
if (nextRequest.launch.appId !== request.launch.appId ||
|
|
332
|
+
nextRequest.launch.releaseId !== request.launch.releaseId ||
|
|
333
|
+
nextRequest.launch.surfaceId !== request.launch.surfaceId)
|
|
334
|
+
throw new Error("App identity cannot change during reconfiguration");
|
|
335
|
+
await surface.reconfigure(nextRequest.launch);
|
|
336
|
+
};
|
|
337
|
+
return new MountedChildHandle(surface, childCommands, reconfigureCurrent, async () => {
|
|
338
|
+
try {
|
|
339
|
+
surface.dispose();
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
await release();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
await release();
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
reportAgentSessionSelection(windowId, selection) {
|
|
352
|
+
const parsed = Schema.decodeUnknownSync(nonBlank)(windowId);
|
|
353
|
+
this.#options.reportAgentSessionSelection(parsed, selection);
|
|
354
|
+
return Promise.resolve();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import type { AppCommand, AppOpenRequest, FileNavigationInput, AppNavigationActivation, AppNavigationState, AppStatusAction, AppStatusItem } from "@fungi.computer/agar";
|
|
4
|
+
import type { AppOpenInput, AppRoleChoice } from "@fungi.computer/agar/apps";
|
|
5
|
+
import type { WhistleExecutionOrigin, WhistleJsonValue } from "@fungi.computer/whistle";
|
|
6
|
+
/** A request to resolve and mount one child App, by its own identity. */
|
|
7
|
+
export type WorkbenchChildLaunch = Readonly<{
|
|
8
|
+
/** The window that owns the child. */
|
|
9
|
+
windowId: string;
|
|
10
|
+
/** The App to mount. */
|
|
11
|
+
appId: string;
|
|
12
|
+
/** The Release of the App to mount. */
|
|
13
|
+
releaseId: string;
|
|
14
|
+
/** The App surface to mount. */
|
|
15
|
+
surfaceId: string;
|
|
16
|
+
/** The address the child's surface is opened at. */
|
|
17
|
+
href: string;
|
|
18
|
+
/** The title shown for the window. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** The App-open handoff delivered to the child at launch, or null. */
|
|
21
|
+
openRequest: AppOpenRequest | null;
|
|
22
|
+
}>;
|
|
23
|
+
/** One invocation of a mounted child's own Release-declared command. */
|
|
24
|
+
export type WorkbenchCommandInvocation = Readonly<{
|
|
25
|
+
/** The command's input, when it takes any. */
|
|
26
|
+
arguments?: WhistleJsonValue;
|
|
27
|
+
/** What caused the command to run, such as a key press. */
|
|
28
|
+
origin: WhistleExecutionOrigin;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Events a mounted child raises toward its dashboard. A child's permission
|
|
32
|
+
* requests never reach the dashboard: the host asks the person itself. The
|
|
33
|
+
* host validates a
|
|
34
|
+
* child's commands against that child's own Release declaration before
|
|
35
|
+
* forwarding them, so the dashboard can present them but never widen them.
|
|
36
|
+
*/
|
|
37
|
+
export type WorkbenchChildEvents = Readonly<{
|
|
38
|
+
/** Ask this window's shell to open its existing Computer picker. */
|
|
39
|
+
onComputerChoice?: () => Promise<void>;
|
|
40
|
+
/** Ask the embedding host to follow a file navigation request. */
|
|
41
|
+
onNavigation?: (input: FileNavigationInput) => Promise<void>;
|
|
42
|
+
/** Ask the embedding host to open an App through its own admission policy. */
|
|
43
|
+
onAppOpen?: (input: AppOpenInput) => Promise<void>;
|
|
44
|
+
/** Ask the embedding host which App its role routing would open for a role. */
|
|
45
|
+
onAppRole?: (role: string) => Promise<AppRoleChoice>;
|
|
46
|
+
/** Report a changed child selection; the host returns whether it handled it. */
|
|
47
|
+
onSelectionChanged?: () => Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* Replace the child's presented command set; an empty list clears it. Run
|
|
50
|
+
* one through the child's handle with `invokeCommand`.
|
|
51
|
+
*/
|
|
52
|
+
onCommands?: (commands: readonly AppCommand[]) => Promise<void>;
|
|
53
|
+
/** A reserved shell chord (the command palette) pressed inside the child. */
|
|
54
|
+
onReservedChord?: (input: Readonly<{
|
|
55
|
+
id: string;
|
|
56
|
+
repeat: boolean;
|
|
57
|
+
}>) => Promise<void>;
|
|
58
|
+
/** The child gained (`true`) or lost (`false`) input focus. */
|
|
59
|
+
onFocus?: (focused: boolean) => Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* The child's latest parsed navigation state; `null` once it disconnects.
|
|
62
|
+
* The dashboard shows a Back affordance from it.
|
|
63
|
+
*/
|
|
64
|
+
onNavigationState?: (state: AppNavigationState | null) => Promise<void>;
|
|
65
|
+
/** The child's latest status readouts; an empty list once it disconnects. */
|
|
66
|
+
onStatusItems?: (items: readonly AppStatusItem[]) => Promise<void>;
|
|
67
|
+
}>;
|
|
68
|
+
/** One mounted child App; every method proxies to its real host-owned mount. */
|
|
69
|
+
export type WorkbenchChildHandle = {
|
|
70
|
+
/** Show or hide the mounted child without changing its identity. */
|
|
71
|
+
setVisibility(visibility: "hidden" | "visible"): Promise<void>;
|
|
72
|
+
/** Update the launch descriptor of the existing mount. */
|
|
73
|
+
reconfigure(launch: WorkbenchChildLaunch): Promise<void>;
|
|
74
|
+
/** Ask whether the child permits a user-initiated close. */
|
|
75
|
+
requestClose(): Promise<boolean>;
|
|
76
|
+
/** Ask the child to go back one step in its own history; `true` when it did. */
|
|
77
|
+
requestBack(): Promise<boolean>;
|
|
78
|
+
/** Deliver the person's use of a child's status item; `true` when it acted. */
|
|
79
|
+
activateStatus(action: AppStatusAction): Promise<boolean>;
|
|
80
|
+
/** Deliver the person's use of a child's crumb or header command; `true` when it acted. */
|
|
81
|
+
activateNavigation(activation: AppNavigationActivation): Promise<boolean>;
|
|
82
|
+
/** Run one of the child's currently presented commands. */
|
|
83
|
+
invokeCommand(commandId: string, input: WorkbenchCommandInvocation): Promise<WhistleJsonValue | undefined>;
|
|
84
|
+
/** Release the child mount and its resources. */
|
|
85
|
+
dispose(): Promise<void>;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* A platform destination the host offers the dashboard, such as opening
|
|
89
|
+
* a host settings page. The host owns the labels and the navigation;
|
|
90
|
+
* the dashboard only presents them and asks the host to follow one.
|
|
91
|
+
*/
|
|
92
|
+
export type WorkbenchPlatformDestination = Readonly<{
|
|
93
|
+
/** The destination's id, which the host follows when asked to open it. */
|
|
94
|
+
id: string;
|
|
95
|
+
/** The name shown for the destination. */
|
|
96
|
+
label: string;
|
|
97
|
+
}>;
|
|
98
|
+
/** A declared Gill request addressed to one host-selected App surface. */
|
|
99
|
+
export declare const workbenchGillRequestSchema: Schema.Struct<{
|
|
100
|
+
readonly viewId: Schema.String;
|
|
101
|
+
readonly params: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
|
|
102
|
+
readonly after: Schema.optional<Schema.String>;
|
|
103
|
+
readonly options: Schema.optional<Schema.String>;
|
|
104
|
+
/** The App surface the Gill request is addressed to. */
|
|
105
|
+
readonly surfaceId: Schema.String;
|
|
106
|
+
}>;
|
|
107
|
+
/** Decoded Gill request for a surface in the host directory. */
|
|
108
|
+
export type WorkbenchGillRequest = typeof workbenchGillRequestSchema.Type;
|
|
109
|
+
/** The member chooses a declared Gill and size in host-rendered Settings. */
|
|
110
|
+
export declare const desktopGillSelectionSchema: z.ZodObject<{
|
|
111
|
+
surfaceId: z.ZodString;
|
|
112
|
+
gillId: z.ZodString;
|
|
113
|
+
}, z.core.$strict>;
|
|
114
|
+
/** The identity of a Gill selected in a host-rendered settings view. */
|
|
115
|
+
export type DesktopGillSelection = z.output<typeof desktopGillSelectionSchema>;
|
|
116
|
+
/** The dashboard layout sizes currently assigned to one Gill. */
|
|
117
|
+
export declare const desktopGillSizesSchema: z.ZodArray<z.ZodEnum<{
|
|
118
|
+
/** Small: one dashboard cell. */
|
|
119
|
+
readonly S: "S";
|
|
120
|
+
/** Medium: two cells wide, one tall. */
|
|
121
|
+
readonly M: "M";
|
|
122
|
+
/** Large: two cells wide, two tall. */
|
|
123
|
+
readonly L: "L";
|
|
124
|
+
}>>;
|
|
125
|
+
/** A selected Gill and the layout size requested for a new placement. */
|
|
126
|
+
export declare const desktopGillAdditionSchema: z.ZodObject<{
|
|
127
|
+
surfaceId: z.ZodString;
|
|
128
|
+
gillId: z.ZodString;
|
|
129
|
+
size: z.ZodEnum<{
|
|
130
|
+
/** Small: one dashboard cell. */
|
|
131
|
+
readonly S: "S";
|
|
132
|
+
/** Medium: two cells wide, one tall. */
|
|
133
|
+
readonly M: "M";
|
|
134
|
+
/** Large: two cells wide, two tall. */
|
|
135
|
+
readonly L: "L";
|
|
136
|
+
}>;
|
|
137
|
+
}, z.core.$strict>;
|
|
138
|
+
/** A decoded request to add a Gill placement. */
|
|
139
|
+
export type DesktopGillAddition = z.output<typeof desktopGillAdditionSchema>;
|
|
140
|
+
/** A dashboard supplies its own arranger, not an endpoint for ordinary Apps. */
|
|
141
|
+
export type WorkbenchDesktopSettings = {
|
|
142
|
+
/** Read the sizes currently assigned to this Gill in the dashboard layout. */
|
|
143
|
+
readGill: (input: DesktopGillSelection) => Promise<z.output<typeof desktopGillSizesSchema>>;
|
|
144
|
+
/** Add a Gill at the requested size to the dashboard layout. */
|
|
145
|
+
addGill: (input: DesktopGillAddition) => Promise<void>;
|
|
146
|
+
/** Remove this Gill from the dashboard layout. */
|
|
147
|
+
removeGill: (input: DesktopGillSelection) => Promise<void>;
|
|
148
|
+
};
|
|
149
|
+
/** Shape of the arranger callbacks the shell supplies to its host. */
|
|
150
|
+
export declare const desktopSettingsHandlerSchema: z.ZodPipe<z.ZodUnknown, z.ZodTransform<{
|
|
151
|
+
readGill: z.core.$InferOuterFunctionType<z.core.$ZodTuple<readonly [z.ZodObject<{
|
|
152
|
+
surfaceId: z.ZodString;
|
|
153
|
+
gillId: z.ZodString;
|
|
154
|
+
}, z.core.$strict>], z.core.$ZodFunctionOut>, z.ZodPromise<z.ZodPipe<z.ZodUnknown, z.ZodTransform<("S" | "M" | "L")[], unknown>>>>;
|
|
155
|
+
addGill: z.core.$InferOuterFunctionType<z.core.$ZodTuple<readonly [z.ZodObject<{
|
|
156
|
+
surfaceId: z.ZodString;
|
|
157
|
+
gillId: z.ZodString;
|
|
158
|
+
size: z.ZodEnum<{
|
|
159
|
+
/** Small: one dashboard cell. */
|
|
160
|
+
readonly S: "S";
|
|
161
|
+
/** Medium: two cells wide, one tall. */
|
|
162
|
+
readonly M: "M";
|
|
163
|
+
/** Large: two cells wide, two tall. */
|
|
164
|
+
readonly L: "L";
|
|
165
|
+
}>;
|
|
166
|
+
}, z.core.$strict>], z.core.$ZodFunctionOut>, z.ZodPromise<z.ZodPipe<z.ZodUnknown, z.ZodTransform<void, unknown>>>>;
|
|
167
|
+
removeGill: z.core.$InferOuterFunctionType<z.core.$ZodTuple<readonly [z.ZodObject<{
|
|
168
|
+
surfaceId: z.ZodString;
|
|
169
|
+
gillId: z.ZodString;
|
|
170
|
+
}, z.core.$strict>], z.core.$ZodFunctionOut>, z.ZodPromise<z.ZodPipe<z.ZodUnknown, z.ZodTransform<void, unknown>>>>;
|
|
171
|
+
} & {
|
|
172
|
+
[Symbol.dispose](): void;
|
|
173
|
+
}, unknown>>;
|
|
174
|
+
/** A registration lease; close it when the dashboard unmounts. */
|
|
175
|
+
export type WorkbenchDesktopRegistration = {
|
|
176
|
+
/** Release this host registration. */
|
|
177
|
+
close(): Promise<void>;
|
|
178
|
+
};
|
|
179
|
+
/** A host-owned change subscription; close it when no longer visible. */
|
|
180
|
+
export type WorkbenchGillWatch = {
|
|
181
|
+
/** Release this host registration. */
|
|
182
|
+
close(): Promise<void>;
|
|
183
|
+
};
|
|
184
|
+
/** Runtime check for an App-open handoff arriving across an untrusted boundary. */
|
|
185
|
+
export declare const appOpenRequestSchema: Schema.Union<readonly [Schema.Struct<{
|
|
186
|
+
/** Marks a handoff that composes text. */
|
|
187
|
+
readonly kind: Schema.Literal<"compose">;
|
|
188
|
+
/** The id of the handoff. */
|
|
189
|
+
readonly requestId: Schema.String;
|
|
190
|
+
/** The text to compose. */
|
|
191
|
+
readonly text: Schema.String;
|
|
192
|
+
}>, Schema.Struct<{
|
|
193
|
+
/** Marks a handoff that navigates. */
|
|
194
|
+
readonly kind: Schema.Literal<"navigate">;
|
|
195
|
+
/** The id of the handoff. */
|
|
196
|
+
readonly requestId: Schema.String;
|
|
197
|
+
/** The navigation parameters. */
|
|
198
|
+
readonly params: Schema.$Array<Schema.Struct<{
|
|
199
|
+
/** The parameter's key. */
|
|
200
|
+
readonly key: Schema.String;
|
|
201
|
+
/** The parameter's value. */
|
|
202
|
+
readonly value: Schema.String;
|
|
203
|
+
}>>;
|
|
204
|
+
}>]>;
|
|
205
|
+
/** One Team Computer in a host's inventory: the wire of `computers()`. */
|
|
206
|
+
export declare const teamComputerSchema: z.ZodObject<{
|
|
207
|
+
computerId: z.ZodString;
|
|
208
|
+
name: z.ZodString;
|
|
209
|
+
createdAt: z.ZodNumber;
|
|
210
|
+
updatedAt: z.ZodNumber;
|
|
211
|
+
}, z.core.$strict>;
|
|
212
|
+
/** A decoded Team Computer inventory record. */
|
|
213
|
+
export type TeamComputer = z.output<typeof teamComputerSchema>;
|
|
214
|
+
/** One page of a host's Team Computer inventory. */
|
|
215
|
+
export declare const teamComputerDirectorySchema: z.ZodObject<{
|
|
216
|
+
computers: z.ZodArray<z.ZodObject<{
|
|
217
|
+
computerId: z.ZodString;
|
|
218
|
+
name: z.ZodString;
|
|
219
|
+
createdAt: z.ZodNumber;
|
|
220
|
+
updatedAt: z.ZodNumber;
|
|
221
|
+
}, z.core.$strict>>;
|
|
222
|
+
nextOffset: z.ZodNullable<z.ZodNumber>;
|
|
223
|
+
}, z.core.$strict>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { rpcDataSchema, rpcHandleSchema, rpcMethod, rpcVoid, } from "@fungi.computer/agar/rpc";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { gillRequestSchema } from "@fungi.computer/gills";
|
|
5
|
+
/** A declared Gill request addressed to one host-selected App surface. */
|
|
6
|
+
export const workbenchGillRequestSchema = Schema.Struct({
|
|
7
|
+
/** The App surface the Gill request is addressed to. */
|
|
8
|
+
surfaceId: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(513)),
|
|
9
|
+
...gillRequestSchema.fields,
|
|
10
|
+
});
|
|
11
|
+
/** The member chooses a declared Gill and size in host-rendered Settings. */
|
|
12
|
+
export const desktopGillSelectionSchema = z.strictObject({
|
|
13
|
+
/** The App surface that declares the Gill. */
|
|
14
|
+
surfaceId: z.string().min(1).max(256),
|
|
15
|
+
/** The id of the Gill. */
|
|
16
|
+
gillId: z.string().min(1).max(80),
|
|
17
|
+
});
|
|
18
|
+
/** The dashboard layout sizes currently assigned to one Gill. */
|
|
19
|
+
export const desktopGillSizesSchema = z
|
|
20
|
+
.array(z.enum({
|
|
21
|
+
/** Small: one dashboard cell. */
|
|
22
|
+
S: "S",
|
|
23
|
+
/** Medium: two cells wide, one tall. */
|
|
24
|
+
M: "M",
|
|
25
|
+
/** Large: two cells wide, two tall. */
|
|
26
|
+
L: "L",
|
|
27
|
+
}))
|
|
28
|
+
.max(24);
|
|
29
|
+
/** A selected Gill and the layout size requested for a new placement. */
|
|
30
|
+
export const desktopGillAdditionSchema = desktopGillSelectionSchema.extend({
|
|
31
|
+
/** The layout size for the new placement. */
|
|
32
|
+
size: z.enum({
|
|
33
|
+
/** Small: one dashboard cell. */
|
|
34
|
+
S: "S",
|
|
35
|
+
/** Medium: two cells wide, one tall. */
|
|
36
|
+
M: "M",
|
|
37
|
+
/** Large: two cells wide, two tall. */
|
|
38
|
+
L: "L",
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
/** Shape of the arranger callbacks the shell supplies to its host. */
|
|
42
|
+
export const desktopSettingsHandlerSchema = rpcHandleSchema({
|
|
43
|
+
/** Read the sizes assigned to a Gill. */
|
|
44
|
+
readGill: rpcMethod(rpcDataSchema(desktopGillSizesSchema)).input([
|
|
45
|
+
desktopGillSelectionSchema,
|
|
46
|
+
]),
|
|
47
|
+
/** Add a Gill placement. */
|
|
48
|
+
addGill: rpcVoid.input([desktopGillAdditionSchema]),
|
|
49
|
+
/** Remove a Gill from the layout. */
|
|
50
|
+
removeGill: rpcVoid.input([desktopGillSelectionSchema]),
|
|
51
|
+
});
|
|
52
|
+
/** Runtime check for an App-open handoff arriving across an untrusted boundary. */
|
|
53
|
+
export const appOpenRequestSchema = Schema.Union([
|
|
54
|
+
Schema.Struct({
|
|
55
|
+
/** Marks a handoff that composes text. */
|
|
56
|
+
kind: Schema.Literal("compose"),
|
|
57
|
+
/** The id of the handoff. */
|
|
58
|
+
requestId: Schema.String,
|
|
59
|
+
/** The text to compose. */
|
|
60
|
+
text: Schema.String,
|
|
61
|
+
}),
|
|
62
|
+
Schema.Struct({
|
|
63
|
+
/** Marks a handoff that navigates. */
|
|
64
|
+
kind: Schema.Literal("navigate"),
|
|
65
|
+
/** The id of the handoff. */
|
|
66
|
+
requestId: Schema.String,
|
|
67
|
+
/** The navigation parameters. */
|
|
68
|
+
params: Schema.Array(Schema.Struct({
|
|
69
|
+
/** The parameter's key. */
|
|
70
|
+
key: Schema.String,
|
|
71
|
+
/** The parameter's value. */
|
|
72
|
+
value: Schema.String,
|
|
73
|
+
})),
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
/** One Team Computer in a host's inventory: the wire of `computers()`. */
|
|
77
|
+
export const teamComputerSchema = z.strictObject({
|
|
78
|
+
/** The Computer's id. */
|
|
79
|
+
computerId: z.string().min(1),
|
|
80
|
+
/** The name shown for the Computer. */
|
|
81
|
+
name: z.string().min(1).max(256),
|
|
82
|
+
/** When the Computer was created. */
|
|
83
|
+
createdAt: z.number().int().nonnegative(),
|
|
84
|
+
/** When the Computer last changed. */
|
|
85
|
+
updatedAt: z.number().int().nonnegative(),
|
|
86
|
+
});
|
|
87
|
+
/** One page of a host's Team Computer inventory. */
|
|
88
|
+
export const teamComputerDirectorySchema = z.strictObject({
|
|
89
|
+
/** The Computers in this page. */
|
|
90
|
+
computers: z.array(teamComputerSchema),
|
|
91
|
+
/** The offset of the next page, or null when this was the last. */
|
|
92
|
+
nextOffset: z.number().int().nonnegative().nullable(),
|
|
93
|
+
});
|