@frockbot/plugin-computer 0.2.3 → 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-computer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@cordisjs/client": "0.8.2",
|
|
32
|
-
"@frockbot/client-core": "0.2.
|
|
33
|
-
"@frockbot/client-ui": "0.2.
|
|
34
|
-
"@frockbot/computer-core": "0.2.
|
|
35
|
-
"@frockbot/computer-host-runtime": "0.2.
|
|
36
|
-
"@frockbot/kernel-agent-loop": "0.2.
|
|
37
|
-
"@frockbot/kernel-contracts": "0.2.
|
|
38
|
-
"@frockbot/plugin-prompt": "0.2.
|
|
39
|
-
"@frockbot/plugin-shell": "0.2.
|
|
40
|
-
"@frockbot/plugin-tools": "0.2.
|
|
32
|
+
"@frockbot/client-core": "0.2.4",
|
|
33
|
+
"@frockbot/client-ui": "0.2.4",
|
|
34
|
+
"@frockbot/computer-core": "0.2.4",
|
|
35
|
+
"@frockbot/computer-host-runtime": "0.2.4",
|
|
36
|
+
"@frockbot/kernel-agent-loop": "0.2.4",
|
|
37
|
+
"@frockbot/kernel-contracts": "0.2.4",
|
|
38
|
+
"@frockbot/plugin-prompt": "0.2.4",
|
|
39
|
+
"@frockbot/plugin-shell": "0.2.4",
|
|
40
|
+
"@frockbot/plugin-tools": "0.2.4",
|
|
41
41
|
"cordis": "4.0.0-rc.8",
|
|
42
42
|
"vue": "3.5.41"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@frockbot/plugin-models": "0.2.
|
|
46
|
-
"@frockbot/plugin-testkit": "0.2.
|
|
45
|
+
"@frockbot/plugin-models": "0.2.4",
|
|
46
|
+
"@frockbot/plugin-testkit": "0.2.4",
|
|
47
47
|
"@types/bun": "1.4.0",
|
|
48
48
|
"@types/node": "26.2.0",
|
|
49
49
|
"@vitejs/plugin-vue": "6.0.8",
|
package/src/bot.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type {
|
|
3
|
+
ComputerConnectionOptionsV1,
|
|
3
4
|
ComputerControlLease,
|
|
4
5
|
ComputerHandle,
|
|
5
6
|
} from "@frockbot/computer-core";
|
|
@@ -59,7 +60,7 @@ function command(
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function fakeHandle(options: {
|
|
62
|
-
presence?(): Promise<{
|
|
63
|
+
presence?(options?: ComputerConnectionOptionsV1): Promise<{
|
|
63
64
|
id: string;
|
|
64
65
|
url: string;
|
|
65
66
|
expiresAt: string;
|
|
@@ -100,6 +101,113 @@ function fakeHandle(options: {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
describe("Computer Bot Durable Object Contribution", () => {
|
|
104
|
+
test("records provider progress durably and projects its ordered steps", async () => {
|
|
105
|
+
const storage = new MemoryStorage();
|
|
106
|
+
let contribution: ReturnType<typeof createComputerBotBackendContribution>;
|
|
107
|
+
contribution = createComputerBotBackendContribution({
|
|
108
|
+
storage,
|
|
109
|
+
configured: true,
|
|
110
|
+
providerLabel: "Fake Computer",
|
|
111
|
+
openComputer: () =>
|
|
112
|
+
Promise.resolve(
|
|
113
|
+
fakeHandle({
|
|
114
|
+
presence: async (options) => {
|
|
115
|
+
expect(await contribution.read("user-1", "scout")).toMatchObject({
|
|
116
|
+
phase: "provisioning",
|
|
117
|
+
progress: {
|
|
118
|
+
kind: "connect",
|
|
119
|
+
steps: [{ id: "waking", status: "active" }],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
await options?.onProgress?.({
|
|
123
|
+
version: 1,
|
|
124
|
+
kind: "connect",
|
|
125
|
+
step: "starting-desktop",
|
|
126
|
+
label: "Starting the desktop",
|
|
127
|
+
index: 3,
|
|
128
|
+
total: 5,
|
|
129
|
+
});
|
|
130
|
+
expect(await contribution.read("user-1", "scout")).toMatchObject({
|
|
131
|
+
phase: "provisioning",
|
|
132
|
+
progress: {
|
|
133
|
+
version: 1,
|
|
134
|
+
kind: "connect",
|
|
135
|
+
steps: [
|
|
136
|
+
{ id: "waking", status: "complete" },
|
|
137
|
+
{ id: "attaching", status: "complete" },
|
|
138
|
+
{ id: "starting-desktop", status: "active" },
|
|
139
|
+
{ id: "minting-viewer", status: "pending" },
|
|
140
|
+
{ id: "connecting", status: "pending" },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
id: "viewer-1",
|
|
146
|
+
url: "https://viewer.invalid/secret",
|
|
147
|
+
expiresAt: "2026-09-03T00:01:30.000Z",
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
152
|
+
now: () => new Date("2026-09-03T00:00:00.000Z"),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await contribution.execute(
|
|
156
|
+
"user-1",
|
|
157
|
+
"scout",
|
|
158
|
+
command("connect", "connect-progress"),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(
|
|
162
|
+
(storage.values.get(COMPUTER_PROVIDER_RECORD_KEY) as { version: number })
|
|
163
|
+
.version,
|
|
164
|
+
).toBe(2);
|
|
165
|
+
expect(
|
|
166
|
+
(await contribution.read("user-1", "scout")).progress,
|
|
167
|
+
).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("migrates a V1 provider record and writes V2 on the next change", async () => {
|
|
171
|
+
const storage = new MemoryStorage();
|
|
172
|
+
storage.values.set(COMPUTER_PROVIDER_RECORD_KEY, {
|
|
173
|
+
version: 1,
|
|
174
|
+
phase: "provisioning",
|
|
175
|
+
message: "An older durable wake",
|
|
176
|
+
recordedAt: "2026-09-02T23:59:00.000Z",
|
|
177
|
+
});
|
|
178
|
+
const contribution = createComputerBotBackendContribution({
|
|
179
|
+
storage,
|
|
180
|
+
configured: true,
|
|
181
|
+
providerLabel: "Fake Computer",
|
|
182
|
+
openComputer: () =>
|
|
183
|
+
Promise.resolve(
|
|
184
|
+
fakeHandle({
|
|
185
|
+
presence: () =>
|
|
186
|
+
Promise.resolve({
|
|
187
|
+
id: "viewer-1",
|
|
188
|
+
url: "https://viewer.invalid/secret",
|
|
189
|
+
expiresAt: "2026-09-03T00:01:30.000Z",
|
|
190
|
+
}),
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
now: () => new Date("2026-09-03T00:00:00.000Z"),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(await contribution.read("user-1", "scout")).toMatchObject({
|
|
197
|
+
phase: "provisioning",
|
|
198
|
+
message: "An older durable wake",
|
|
199
|
+
});
|
|
200
|
+
await contribution.execute(
|
|
201
|
+
"user-1",
|
|
202
|
+
"scout",
|
|
203
|
+
command("connect", "connect-after-v1"),
|
|
204
|
+
);
|
|
205
|
+
expect(storage.values.get(COMPUTER_PROVIDER_RECORD_KEY)).toMatchObject({
|
|
206
|
+
version: 2,
|
|
207
|
+
phase: "ready",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
103
211
|
test("commits intent before it asks the provider and replays one receipt", async () => {
|
|
104
212
|
const storage = new MemoryStorage();
|
|
105
213
|
let calls = 0;
|
|
@@ -229,6 +337,7 @@ describe("Computer Bot Durable Object Contribution", () => {
|
|
|
229
337
|
phase: "updating",
|
|
230
338
|
message: "Updating the Computer runtime",
|
|
231
339
|
viewerSession: { id: "viewer-1" },
|
|
340
|
+
progress: { kind: "update" },
|
|
232
341
|
});
|
|
233
342
|
});
|
|
234
343
|
|
|
@@ -264,6 +373,10 @@ describe("Computer Bot Durable Object Contribution", () => {
|
|
|
264
373
|
expect(await contribution.read("user-1", "scout")).toMatchObject({
|
|
265
374
|
phase: "updating",
|
|
266
375
|
message: "Updating the Computer runtime",
|
|
376
|
+
progress: {
|
|
377
|
+
kind: "update",
|
|
378
|
+
steps: [{ label: "Updating the Computer runtime", status: "active" }],
|
|
379
|
+
},
|
|
267
380
|
});
|
|
268
381
|
});
|
|
269
382
|
|
package/src/bot.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
computerBotPathKeyV1,
|
|
9
9
|
ComputerError,
|
|
10
10
|
decodeComputerDoctorReportV1,
|
|
11
|
+
type ComputerConnectionProgressV1,
|
|
11
12
|
type ComputerControlLease,
|
|
12
13
|
type ComputerHandle,
|
|
13
14
|
} from "@frockbot/computer-core";
|
|
@@ -27,11 +28,13 @@ import {
|
|
|
27
28
|
computerUpdateLabelV1,
|
|
28
29
|
decodeComputerCommandReceiptV1,
|
|
29
30
|
decodeComputerCommandV1,
|
|
31
|
+
decodeComputerProgressViewV1,
|
|
30
32
|
type ComputerCommandReceiptV1,
|
|
31
33
|
type ComputerCommandV1,
|
|
32
34
|
type ComputerDoctorViewV1,
|
|
33
35
|
type ComputerPhase,
|
|
34
36
|
type ComputerProjectionV1,
|
|
37
|
+
type ComputerProgressViewV1,
|
|
35
38
|
type ComputerScreenshotViewV1,
|
|
36
39
|
type ComputerViewerSessionViewV1,
|
|
37
40
|
} from "./protocol.js";
|
|
@@ -82,11 +85,15 @@ interface StoredViewerV1 {
|
|
|
82
85
|
expiresAt: string;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
type StoredProviderPhase =
|
|
89
|
+
"provisioning" | "updating" | "ready" | "disconnected" | "error";
|
|
90
|
+
|
|
91
|
+
interface StoredProviderAnswerV2 {
|
|
92
|
+
version: 2;
|
|
93
|
+
phase: StoredProviderPhase;
|
|
88
94
|
message: string;
|
|
89
95
|
recordedAt: string;
|
|
96
|
+
progress?: ComputerProgressViewV1;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
interface StoredIntentV1 {
|
|
@@ -161,16 +168,26 @@ function decodeStoredViewer(value: unknown): StoredViewerV1 {
|
|
|
161
168
|
};
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
/** Migrates the released V1 shape at the storage read seam. */
|
|
172
|
+
function decodeStoredProvider(value: unknown): StoredProviderAnswerV2 {
|
|
165
173
|
const record = object(value, "Computer provider record");
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
if (record.version === 1) {
|
|
175
|
+
exact(
|
|
176
|
+
record,
|
|
177
|
+
["version", "phase", "message", "recordedAt"],
|
|
178
|
+
[],
|
|
179
|
+
"Computer provider record",
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
exact(
|
|
183
|
+
record,
|
|
184
|
+
["version", "phase", "message", "recordedAt"],
|
|
185
|
+
["progress"],
|
|
186
|
+
"Computer provider record",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
172
189
|
if (
|
|
173
|
-
record.version !== 1 ||
|
|
190
|
+
(record.version !== 1 && record.version !== 2) ||
|
|
174
191
|
(record.phase !== "provisioning" &&
|
|
175
192
|
record.phase !== "updating" &&
|
|
176
193
|
record.phase !== "ready" &&
|
|
@@ -180,13 +197,67 @@ function decodeStoredProvider(value: unknown): StoredProviderAnswerV1 {
|
|
|
180
197
|
throw new Error("Computer provider record is corrupt");
|
|
181
198
|
}
|
|
182
199
|
return {
|
|
183
|
-
version:
|
|
200
|
+
version: 2,
|
|
184
201
|
phase: record.phase,
|
|
185
202
|
message: storedText(record.message, "Computer provider message"),
|
|
186
203
|
recordedAt: storedTimestamp(
|
|
187
204
|
record.recordedAt,
|
|
188
205
|
"Computer provider recordedAt",
|
|
189
206
|
),
|
|
207
|
+
...(record.version === 2 && record.progress !== undefined
|
|
208
|
+
? { progress: decodeComputerProgressViewV1(record.progress) }
|
|
209
|
+
: {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const CONNECT_PROGRESS_STEPS = [
|
|
214
|
+
{ id: "waking", label: "Waking the Computer" },
|
|
215
|
+
{ id: "attaching", label: "Attaching the Bot" },
|
|
216
|
+
{ id: "starting-desktop", label: "Starting the desktop" },
|
|
217
|
+
{ id: "minting-viewer", label: "Minting the secure viewer" },
|
|
218
|
+
{ id: "connecting", label: "Connecting to the desktop" },
|
|
219
|
+
] as const;
|
|
220
|
+
|
|
221
|
+
function projectedProgress(
|
|
222
|
+
progress: ComputerConnectionProgressV1,
|
|
223
|
+
startedAt: string,
|
|
224
|
+
updatedAt: string,
|
|
225
|
+
): ComputerProgressViewV1 {
|
|
226
|
+
if (progress.kind === "update") {
|
|
227
|
+
return {
|
|
228
|
+
version: 1,
|
|
229
|
+
kind: "update",
|
|
230
|
+
startedAt,
|
|
231
|
+
updatedAt,
|
|
232
|
+
index: progress.index,
|
|
233
|
+
total: progress.total,
|
|
234
|
+
steps: [
|
|
235
|
+
{
|
|
236
|
+
version: 1,
|
|
237
|
+
id: progress.step,
|
|
238
|
+
label: progress.label,
|
|
239
|
+
status: "active",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
version: 1,
|
|
246
|
+
kind: "connect",
|
|
247
|
+
startedAt,
|
|
248
|
+
updatedAt,
|
|
249
|
+
index: progress.index,
|
|
250
|
+
total: progress.total,
|
|
251
|
+
steps: CONNECT_PROGRESS_STEPS.map((step, index) => ({
|
|
252
|
+
version: 1,
|
|
253
|
+
...step,
|
|
254
|
+
status:
|
|
255
|
+
index + 1 < progress.index
|
|
256
|
+
? ("complete" as const)
|
|
257
|
+
: index + 1 === progress.index
|
|
258
|
+
? ("active" as const)
|
|
259
|
+
: ("pending" as const),
|
|
260
|
+
})),
|
|
190
261
|
};
|
|
191
262
|
}
|
|
192
263
|
|
|
@@ -414,8 +485,33 @@ export class ComputerBotBackendContribution {
|
|
|
414
485
|
this.#liveViewer = undefined;
|
|
415
486
|
await this.host.storage.delete(COMPUTER_VIEWER_RECORD_KEY);
|
|
416
487
|
}
|
|
488
|
+
const recorded = updating
|
|
489
|
+
? await this.host.storage.get<unknown>(COMPUTER_PROVIDER_RECORD_KEY)
|
|
490
|
+
: undefined;
|
|
491
|
+
const previousProgress =
|
|
492
|
+
recorded === undefined
|
|
493
|
+
? undefined
|
|
494
|
+
: decodeStoredProvider(recorded).progress;
|
|
495
|
+
const recordedAt = this.now().toISOString();
|
|
496
|
+
const updateLabel = computerUpdateLabelV1(failure) ?? failure;
|
|
497
|
+
const progress = updating
|
|
498
|
+
? previousProgress?.kind === "update"
|
|
499
|
+
? previousProgress
|
|
500
|
+
: projectedProgress(
|
|
501
|
+
{
|
|
502
|
+
version: 1,
|
|
503
|
+
kind: "update",
|
|
504
|
+
step: "updating",
|
|
505
|
+
label: updateLabel,
|
|
506
|
+
index: 1,
|
|
507
|
+
total: 1,
|
|
508
|
+
},
|
|
509
|
+
previousProgress?.startedAt ?? recordedAt,
|
|
510
|
+
recordedAt,
|
|
511
|
+
)
|
|
512
|
+
: undefined;
|
|
417
513
|
await this.host.storage.put(COMPUTER_PROVIDER_RECORD_KEY, {
|
|
418
|
-
version:
|
|
514
|
+
version: 2,
|
|
419
515
|
phase:
|
|
420
516
|
command.type === "refreshViewer"
|
|
421
517
|
? "disconnected"
|
|
@@ -426,10 +522,11 @@ export class ComputerBotBackendContribution {
|
|
|
426
522
|
command.type === "refreshViewer"
|
|
427
523
|
? `Viewer disconnected: ${failure}`
|
|
428
524
|
: updating
|
|
429
|
-
?
|
|
525
|
+
? updateLabel
|
|
430
526
|
: failure,
|
|
431
|
-
recordedAt
|
|
432
|
-
|
|
527
|
+
recordedAt,
|
|
528
|
+
...(updating && progress ? { progress } : {}),
|
|
529
|
+
} satisfies StoredProviderAnswerV2);
|
|
433
530
|
return this.settle(command, admitted.fingerprint, "rejected", failure);
|
|
434
531
|
}
|
|
435
532
|
}
|
|
@@ -438,12 +535,26 @@ export class ComputerBotBackendContribution {
|
|
|
438
535
|
userId: string,
|
|
439
536
|
command: ComputerCommandV1,
|
|
440
537
|
): Promise<void> {
|
|
538
|
+
const startedAt = this.now().toISOString();
|
|
539
|
+
let progress = projectedProgress(
|
|
540
|
+
{
|
|
541
|
+
version: 1,
|
|
542
|
+
kind: "connect",
|
|
543
|
+
step: "waking",
|
|
544
|
+
label: "Waking the Computer",
|
|
545
|
+
index: 1,
|
|
546
|
+
total: CONNECT_PROGRESS_STEPS.length,
|
|
547
|
+
},
|
|
548
|
+
startedAt,
|
|
549
|
+
startedAt,
|
|
550
|
+
);
|
|
441
551
|
await this.host.storage.put(COMPUTER_PROVIDER_RECORD_KEY, {
|
|
442
|
-
version:
|
|
552
|
+
version: 2,
|
|
443
553
|
phase: "provisioning",
|
|
444
554
|
message: "Waking and preparing the Computer…",
|
|
445
|
-
recordedAt:
|
|
446
|
-
|
|
555
|
+
recordedAt: startedAt,
|
|
556
|
+
progress,
|
|
557
|
+
} satisfies StoredProviderAnswerV2);
|
|
447
558
|
const session = await this.withComputer(
|
|
448
559
|
userId,
|
|
449
560
|
command,
|
|
@@ -451,8 +562,22 @@ export class ComputerBotBackendContribution {
|
|
|
451
562
|
if (!computer.presence) {
|
|
452
563
|
throw new Error("The selected Computer does not support presence");
|
|
453
564
|
}
|
|
565
|
+
const report = async (
|
|
566
|
+
next: ComputerConnectionProgressV1,
|
|
567
|
+
): Promise<void> => {
|
|
568
|
+
const updatedAt = this.now().toISOString();
|
|
569
|
+
progress = projectedProgress(next, startedAt, updatedAt);
|
|
570
|
+
await this.host.storage.put(COMPUTER_PROVIDER_RECORD_KEY, {
|
|
571
|
+
version: 2,
|
|
572
|
+
phase: next.kind === "update" ? "updating" : "provisioning",
|
|
573
|
+
message: next.label,
|
|
574
|
+
recordedAt: updatedAt,
|
|
575
|
+
progress,
|
|
576
|
+
} satisfies StoredProviderAnswerV2);
|
|
577
|
+
};
|
|
454
578
|
return computer.presence.connect({
|
|
455
579
|
effectId: `computer:${command.commandId}:connect`,
|
|
580
|
+
onProgress: report,
|
|
456
581
|
});
|
|
457
582
|
},
|
|
458
583
|
);
|
|
@@ -465,14 +590,30 @@ export class ComputerBotBackendContribution {
|
|
|
465
590
|
expiresAt: session.expiresAt,
|
|
466
591
|
} satisfies StoredViewerV1;
|
|
467
592
|
const updateLabel = computerUpdateLabelV1(session.message);
|
|
593
|
+
const recordedAt = this.now().toISOString();
|
|
594
|
+
if (updateLabel && progress.kind !== "update") {
|
|
595
|
+
progress = projectedProgress(
|
|
596
|
+
{
|
|
597
|
+
version: 1,
|
|
598
|
+
kind: "update",
|
|
599
|
+
step: "updating",
|
|
600
|
+
label: updateLabel,
|
|
601
|
+
index: 1,
|
|
602
|
+
total: 1,
|
|
603
|
+
},
|
|
604
|
+
startedAt,
|
|
605
|
+
recordedAt,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
468
608
|
await this.host.storage.put({
|
|
469
609
|
[COMPUTER_VIEWER_RECORD_KEY]: stored,
|
|
470
610
|
[COMPUTER_PROVIDER_RECORD_KEY]: {
|
|
471
|
-
version:
|
|
611
|
+
version: 2,
|
|
472
612
|
phase: updateLabel ? "updating" : "ready",
|
|
473
613
|
message: updateLabel ?? "Computer ready",
|
|
474
|
-
recordedAt
|
|
475
|
-
|
|
614
|
+
recordedAt,
|
|
615
|
+
...(updateLabel && progress.kind === "update" ? { progress } : {}),
|
|
616
|
+
} satisfies StoredProviderAnswerV2,
|
|
476
617
|
});
|
|
477
618
|
this.#liveViewer = {
|
|
478
619
|
id: session.id,
|
|
@@ -587,11 +728,11 @@ export class ComputerBotBackendContribution {
|
|
|
587
728
|
expiresAt: renewed.expiresAt,
|
|
588
729
|
} satisfies StoredViewerV1,
|
|
589
730
|
[COMPUTER_PROVIDER_RECORD_KEY]: {
|
|
590
|
-
version:
|
|
731
|
+
version: 2,
|
|
591
732
|
phase: "ready",
|
|
592
733
|
message: "Computer ready",
|
|
593
734
|
recordedAt: this.now().toISOString(),
|
|
594
|
-
} satisfies
|
|
735
|
+
} satisfies StoredProviderAnswerV2,
|
|
595
736
|
});
|
|
596
737
|
this.#liveViewer = {
|
|
597
738
|
id: renewed.id,
|
|
@@ -800,6 +941,10 @@ export class ComputerBotBackendContribution {
|
|
|
800
941
|
providerLabel: this.host.providerLabel,
|
|
801
942
|
phase,
|
|
802
943
|
message,
|
|
944
|
+
...(provider?.progress &&
|
|
945
|
+
(phase === "provisioning" || phase === "updating")
|
|
946
|
+
? { progress: provider.progress }
|
|
947
|
+
: {}),
|
|
803
948
|
...(viewerSession ? { viewerSession } : {}),
|
|
804
949
|
...(activeControl
|
|
805
950
|
? {
|
|
@@ -4,6 +4,7 @@ import { parse } from "@vue/compiler-sfc";
|
|
|
4
4
|
import type { ComputerState } from "../shared.js";
|
|
5
5
|
import {
|
|
6
6
|
createComputerViewerActions,
|
|
7
|
+
decodeComputerViewerFrameMessageV1,
|
|
7
8
|
viewerUrlForControlV1,
|
|
8
9
|
} from "./viewer.js";
|
|
9
10
|
|
|
@@ -37,6 +38,28 @@ describe("Computer viewer", () => {
|
|
|
37
38
|
);
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
test("accepts only secret-free viewer status messages", () => {
|
|
42
|
+
expect(
|
|
43
|
+
decodeComputerViewerFrameMessageV1({
|
|
44
|
+
type: "frockbot-viewer",
|
|
45
|
+
state: "connected",
|
|
46
|
+
message: "Desktop connected",
|
|
47
|
+
}),
|
|
48
|
+
).toEqual({
|
|
49
|
+
type: "frockbot-viewer",
|
|
50
|
+
state: "connected",
|
|
51
|
+
message: "Desktop connected",
|
|
52
|
+
});
|
|
53
|
+
expect(
|
|
54
|
+
decodeComputerViewerFrameMessageV1({
|
|
55
|
+
type: "frockbot-viewer",
|
|
56
|
+
state: "connected",
|
|
57
|
+
message: "Desktop connected",
|
|
58
|
+
password: "secret",
|
|
59
|
+
}),
|
|
60
|
+
).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
40
63
|
test("requires confirmation before taking control and Escape closes through shared state", async () => {
|
|
41
64
|
let confirmations = false;
|
|
42
65
|
let takeControl = 0;
|
|
@@ -77,6 +100,8 @@ describe("Computer viewer", () => {
|
|
|
77
100
|
const template = parsed.descriptor.template?.content ?? "";
|
|
78
101
|
|
|
79
102
|
expect(template).toContain(':src="viewerSrc"');
|
|
103
|
+
expect(template).toContain('@load="handleFrameLoad"');
|
|
104
|
+
expect(template).toContain('role="progressbar"');
|
|
80
105
|
expect(template).toContain('@click="actions.requestTakeControl"');
|
|
81
106
|
expect(template).toContain('role="alertdialog"');
|
|
82
107
|
expect(template).toContain(
|