@frockbot/plugin-computer 0.2.2 → 0.2.4
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/package.json +12 -12
- package/src/bot.test.ts +114 -1
- package/src/bot.ts +169 -24
- package/src/client/ComputerCard.test.ts +25 -0
- package/src/client/ComputerViewerOverlay.vue +126 -8
- package/src/client/application.test.ts +36 -2
- package/src/client/application.ts +59 -11
- package/src/client/state-machine.test.ts +33 -0
- package/src/client/state-machine.ts +13 -0
- package/src/client/styles.css +80 -0
- package/src/client/viewer.ts +43 -0
- package/src/protocol.test.ts +77 -0
- package/src/protocol.ts +114 -1
- package/src/shared.ts +2 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
ComputerProtocolDecodeError,
|
|
4
|
+
decodeComputerProjectionV1,
|
|
5
|
+
type ComputerProgressViewV1,
|
|
6
|
+
type ComputerProjectionV1,
|
|
7
|
+
} from "./protocol.js";
|
|
8
|
+
|
|
9
|
+
const projection: ComputerProjectionV1 & { progress: ComputerProgressViewV1 } =
|
|
10
|
+
{
|
|
11
|
+
version: 1,
|
|
12
|
+
botId: "scout",
|
|
13
|
+
providerLabel: "Fake Computer",
|
|
14
|
+
phase: "provisioning",
|
|
15
|
+
message: "Starting the desktop…",
|
|
16
|
+
progress: {
|
|
17
|
+
version: 1,
|
|
18
|
+
kind: "connect",
|
|
19
|
+
startedAt: "2026-09-03T00:00:00.000Z",
|
|
20
|
+
updatedAt: "2026-09-03T00:00:02.000Z",
|
|
21
|
+
index: 2,
|
|
22
|
+
total: 3,
|
|
23
|
+
steps: [
|
|
24
|
+
{
|
|
25
|
+
version: 1,
|
|
26
|
+
id: "waking",
|
|
27
|
+
label: "Waking the Computer",
|
|
28
|
+
status: "complete",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
version: 1,
|
|
32
|
+
id: "starting-desktop",
|
|
33
|
+
label: "Starting the desktop",
|
|
34
|
+
status: "active",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
version: 1,
|
|
38
|
+
id: "minting-viewer",
|
|
39
|
+
label: "Minting the viewer",
|
|
40
|
+
status: "pending",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
screenshots: [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe("Computer projection progress", () => {
|
|
48
|
+
test("decodes the exact ordered V1 progress shape", () => {
|
|
49
|
+
expect(decodeComputerProjectionV1(projection).progress).toEqual(
|
|
50
|
+
projection.progress,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("refuses malformed or extended progress at the client seam", () => {
|
|
55
|
+
expect(() =>
|
|
56
|
+
decodeComputerProjectionV1({
|
|
57
|
+
...projection,
|
|
58
|
+
progress: {
|
|
59
|
+
...projection.progress,
|
|
60
|
+
secret: "must not cross",
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
).toThrow(ComputerProtocolDecodeError);
|
|
64
|
+
expect(() =>
|
|
65
|
+
decodeComputerProjectionV1({
|
|
66
|
+
...projection,
|
|
67
|
+
progress: {
|
|
68
|
+
...projection.progress,
|
|
69
|
+
steps: projection.progress.steps.map((step) => ({
|
|
70
|
+
...step,
|
|
71
|
+
status: "working",
|
|
72
|
+
})),
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
).toThrow(ComputerProtocolDecodeError);
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/protocol.ts
CHANGED
|
@@ -61,6 +61,26 @@ export interface ComputerDoctorViewV1 {
|
|
|
61
61
|
checks: ComputerDoctorCheckViewV1[];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export type ComputerProgressStepStatusV1 = "pending" | "active" | "complete";
|
|
65
|
+
|
|
66
|
+
export interface ComputerProgressStepViewV1 {
|
|
67
|
+
version: 1;
|
|
68
|
+
id: string;
|
|
69
|
+
label: string;
|
|
70
|
+
status: ComputerProgressStepStatusV1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Durable progress projected from the Bot authority; it contains no secrets. */
|
|
74
|
+
export interface ComputerProgressViewV1 {
|
|
75
|
+
version: 1;
|
|
76
|
+
kind: "connect" | "update";
|
|
77
|
+
startedAt: string;
|
|
78
|
+
updatedAt: string;
|
|
79
|
+
index: number;
|
|
80
|
+
total: number;
|
|
81
|
+
steps: ComputerProgressStepViewV1[];
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
export const COMPUTER_PHASES = [
|
|
65
85
|
"unconfigured",
|
|
66
86
|
"idle",
|
|
@@ -92,6 +112,7 @@ export interface ComputerProjectionV1 {
|
|
|
92
112
|
providerLabel: string;
|
|
93
113
|
phase: ComputerPhase;
|
|
94
114
|
message: string;
|
|
115
|
+
progress?: ComputerProgressViewV1;
|
|
95
116
|
viewerSession?: ComputerViewerSessionViewV1;
|
|
96
117
|
controlLease?: ComputerControlLeaseViewV1;
|
|
97
118
|
screenshots: ComputerScreenshotViewV1[];
|
|
@@ -176,6 +197,95 @@ function phase(value: unknown): ComputerPhase {
|
|
|
176
197
|
return decoded;
|
|
177
198
|
}
|
|
178
199
|
|
|
200
|
+
function boundedInteger(
|
|
201
|
+
value: unknown,
|
|
202
|
+
minimum: number,
|
|
203
|
+
maximum: number,
|
|
204
|
+
label: string,
|
|
205
|
+
): number {
|
|
206
|
+
if (
|
|
207
|
+
typeof value !== "number" ||
|
|
208
|
+
!Number.isSafeInteger(value) ||
|
|
209
|
+
value < minimum ||
|
|
210
|
+
value > maximum
|
|
211
|
+
) {
|
|
212
|
+
throw new ComputerProtocolDecodeError(`${label} is invalid`);
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function decodeProgressStepV1(value: unknown): ComputerProgressStepViewV1 {
|
|
218
|
+
const candidate = record(value, "Computer progress step");
|
|
219
|
+
exactKeys(
|
|
220
|
+
candidate,
|
|
221
|
+
["version", "id", "label", "status"],
|
|
222
|
+
[],
|
|
223
|
+
"Computer progress step",
|
|
224
|
+
);
|
|
225
|
+
if (
|
|
226
|
+
candidate.version !== 1 ||
|
|
227
|
+
(candidate.status !== "pending" &&
|
|
228
|
+
candidate.status !== "active" &&
|
|
229
|
+
candidate.status !== "complete")
|
|
230
|
+
) {
|
|
231
|
+
throw new ComputerProtocolDecodeError("Computer progress step is invalid");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
version: 1,
|
|
235
|
+
id: text(candidate.id, "Computer progress step id"),
|
|
236
|
+
label: text(candidate.label, "Computer progress step label"),
|
|
237
|
+
status: candidate.status,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function decodeComputerProgressViewV1(
|
|
242
|
+
value: unknown,
|
|
243
|
+
): ComputerProgressViewV1 {
|
|
244
|
+
const candidate = record(value, "Computer progress");
|
|
245
|
+
exactKeys(
|
|
246
|
+
candidate,
|
|
247
|
+
["version", "kind", "startedAt", "updatedAt", "index", "total", "steps"],
|
|
248
|
+
[],
|
|
249
|
+
"Computer progress",
|
|
250
|
+
);
|
|
251
|
+
if (
|
|
252
|
+
candidate.version !== 1 ||
|
|
253
|
+
(candidate.kind !== "connect" && candidate.kind !== "update") ||
|
|
254
|
+
!Array.isArray(candidate.steps) ||
|
|
255
|
+
candidate.steps.length === 0 ||
|
|
256
|
+
candidate.steps.length > 20
|
|
257
|
+
) {
|
|
258
|
+
throw new ComputerProtocolDecodeError("Computer progress is invalid");
|
|
259
|
+
}
|
|
260
|
+
const total = boundedInteger(
|
|
261
|
+
candidate.total,
|
|
262
|
+
1,
|
|
263
|
+
1_000,
|
|
264
|
+
"Computer progress total",
|
|
265
|
+
);
|
|
266
|
+
const index = boundedInteger(
|
|
267
|
+
candidate.index,
|
|
268
|
+
0,
|
|
269
|
+
total,
|
|
270
|
+
"Computer progress index",
|
|
271
|
+
);
|
|
272
|
+
const steps = candidate.steps.map(decodeProgressStepV1);
|
|
273
|
+
if (steps.filter((step) => step.status === "active").length > 1) {
|
|
274
|
+
throw new ComputerProtocolDecodeError(
|
|
275
|
+
"Computer progress has multiple active steps",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
version: 1,
|
|
280
|
+
kind: candidate.kind,
|
|
281
|
+
startedAt: timestamp(candidate.startedAt, "Computer progress startedAt"),
|
|
282
|
+
updatedAt: timestamp(candidate.updatedAt, "Computer progress updatedAt"),
|
|
283
|
+
index,
|
|
284
|
+
total,
|
|
285
|
+
steps,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
179
289
|
export function decodeComputerCommandV1(value: unknown): ComputerCommandV1 {
|
|
180
290
|
const candidate = record(value, "Computer command");
|
|
181
291
|
exactKeys(
|
|
@@ -328,7 +438,7 @@ export function decodeComputerProjectionV1(
|
|
|
328
438
|
exactKeys(
|
|
329
439
|
candidate,
|
|
330
440
|
["version", "botId", "providerLabel", "phase", "message", "screenshots"],
|
|
331
|
-
["viewerSession", "controlLease", "doctor"],
|
|
441
|
+
["viewerSession", "controlLease", "doctor", "progress"],
|
|
332
442
|
"Computer projection",
|
|
333
443
|
);
|
|
334
444
|
if (candidate.version !== 1 || !Array.isArray(candidate.screenshots)) {
|
|
@@ -343,6 +453,9 @@ export function decodeComputerProjectionV1(
|
|
|
343
453
|
),
|
|
344
454
|
phase: phase(candidate.phase),
|
|
345
455
|
message: text(candidate.message, "Computer projection message"),
|
|
456
|
+
...(candidate.progress === undefined
|
|
457
|
+
? {}
|
|
458
|
+
: { progress: decodeComputerProgressViewV1(candidate.progress) }),
|
|
346
459
|
...(candidate.viewerSession === undefined
|
|
347
460
|
? {}
|
|
348
461
|
: { viewerSession: decodeViewerSessionV1(candidate.viewerSession) }),
|
package/src/shared.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { InjectionKey, Ref } from "vue";
|
|
|
2
2
|
import type {
|
|
3
3
|
ComputerDoctorViewV1,
|
|
4
4
|
ComputerPhase,
|
|
5
|
+
ComputerProgressViewV1,
|
|
5
6
|
ComputerScreenshotViewV1,
|
|
6
7
|
} from "./protocol.js";
|
|
7
8
|
|
|
@@ -30,6 +31,7 @@ export interface ComputerState {
|
|
|
30
31
|
botId: string;
|
|
31
32
|
providerLabel: string;
|
|
32
33
|
message: string;
|
|
34
|
+
progress?: ComputerProgressViewV1;
|
|
33
35
|
viewerUrl?: string;
|
|
34
36
|
/** Whether the one live viewer is open over the hosted shell. */
|
|
35
37
|
expanded: boolean;
|