@frockbot/plugin-computer 0.1.0 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-computer",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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.1.0",
33
- "@frockbot/client-ui": "0.1.0",
34
- "@frockbot/computer-core": "0.1.0",
35
- "@frockbot/computer-host-runtime": "0.1.0",
36
- "@frockbot/kernel-agent-loop": "0.1.0",
37
- "@frockbot/kernel-contracts": "0.1.0",
38
- "@frockbot/plugin-prompt": "0.1.0",
39
- "@frockbot/plugin-shell": "0.1.0",
40
- "@frockbot/plugin-tools": "0.1.0",
32
+ "@frockbot/client-core": "0.1.2",
33
+ "@frockbot/client-ui": "0.1.2",
34
+ "@frockbot/computer-core": "0.1.2",
35
+ "@frockbot/computer-host-runtime": "0.1.2",
36
+ "@frockbot/kernel-agent-loop": "0.1.2",
37
+ "@frockbot/kernel-contracts": "0.1.2",
38
+ "@frockbot/plugin-prompt": "0.1.2",
39
+ "@frockbot/plugin-shell": "0.1.2",
40
+ "@frockbot/plugin-tools": "0.1.2",
41
41
  "cordis": "4.0.0-rc.8",
42
42
  "vue": "3.5.41"
43
43
  },
44
44
  "devDependencies": {
45
- "@frockbot/plugin-models": "0.1.0",
46
- "@frockbot/plugin-testkit": "0.1.0",
45
+ "@frockbot/plugin-models": "0.1.2",
46
+ "@frockbot/plugin-testkit": "0.1.2",
47
47
  "@types/bun": "1.4.0",
48
48
  "@types/node": "26.2.0",
49
49
  "@vitejs/plugin-vue": "6.0.8",
@@ -70,6 +70,7 @@ function mountHostedProvider() {
70
70
  const calls: Array<[string, string | undefined, string | undefined]> = [];
71
71
  const runtime = new FakeRuntime();
72
72
  let phase: Phase = "idle";
73
+ let hostUpdating = false;
73
74
  let controlHeld = false;
74
75
  let renewFails = false;
75
76
  let state: { value: ComputerState } | undefined;
@@ -85,7 +86,9 @@ function mountHostedProvider() {
85
86
  type:
86
87
  "connect" | "takeControl" | "releaseControl" | "refreshViewer";
87
88
  };
88
- if (command.type === "connect") phase = "ready";
89
+ if (command.type === "connect") {
90
+ phase = hostUpdating ? "updating" : "ready";
91
+ }
89
92
  if (command.type === "takeControl") {
90
93
  phase = "human-control";
91
94
  controlHeld = true;
@@ -122,7 +125,9 @@ function mountHostedProvider() {
122
125
  : phase === "updating"
123
126
  ? "Updating the Computer runtime"
124
127
  : "Computer ready",
125
- ...(phase === "idle" || phase === "disconnected"
128
+ ...(phase === "idle" ||
129
+ phase === "disconnected" ||
130
+ phase === "updating"
126
131
  ? {}
127
132
  : {
128
133
  viewerSession: {
@@ -174,6 +179,10 @@ function mountHostedProvider() {
174
179
  },
175
180
  setUpdating() {
176
181
  phase = "updating";
182
+ hostUpdating = true;
183
+ },
184
+ setReady() {
185
+ hostUpdating = false;
177
186
  },
178
187
  dispose() {
179
188
  if (Array.isArray(disposers)) {
@@ -235,7 +244,7 @@ describe("hosted Computer provider", () => {
235
244
  mounted.dispose();
236
245
  });
237
246
 
238
- test("an updating strip click only expands the progress view", async () => {
247
+ test("an updating strip click rejoins the update and lands on ready when it finishes", async () => {
239
248
  const mounted = mountHostedProvider();
240
249
  await flush();
241
250
  mounted.setUpdating();
@@ -246,14 +255,35 @@ describe("hosted Computer provider", () => {
246
255
  message: "Updating the Computer runtime",
247
256
  expanded: false,
248
257
  });
258
+ // A collapsed strip never asks the host anything while it updates.
259
+ expect(postedTypes(mounted.calls)).toEqual([]);
249
260
 
261
+ // Opening rejoins: the host still reports the update, so the phase holds
262
+ // and the progress view is what expands.
250
263
  await mounted.state.openViewer();
251
-
252
264
  expect(mounted.state).toMatchObject({
253
265
  phase: "updating",
254
266
  expanded: true,
267
+ viewerUrl: undefined,
255
268
  });
256
- expect(postedTypes(mounted.calls)).toEqual([]);
269
+ expect(postedTypes(mounted.calls)).toEqual(["connect"]);
270
+
271
+ // Every poll while open asks again, so the durable `updating` record is
272
+ // not the last word once the host has finished.
273
+ mounted.setReady();
274
+ mounted.runtime.tick(PROJECTION_POLL_INTERVAL_MS);
275
+ await flush();
276
+ expect(postedTypes(mounted.calls)).toEqual(["connect", "connect"]);
277
+ expect(mounted.state).toMatchObject({
278
+ phase: "ready",
279
+ expanded: true,
280
+ viewerUrl: "https://viewer.invalid/secret#view_only=1",
281
+ });
282
+
283
+ // Once ready, polls go back to reading only.
284
+ mounted.runtime.tick(PROJECTION_POLL_INTERVAL_MS);
285
+ await flush();
286
+ expect(postedTypes(mounted.calls)).toEqual(["connect", "connect"]);
257
287
  mounted.dispose();
258
288
  });
259
289
 
@@ -163,6 +163,16 @@ export function createComputerClientPlugin(
163
163
  projectionPoll = runtime.setInterval(() => {
164
164
  const selectedBotId = shell.value.activeBotId;
165
165
  if (!selectedBotId || !runtime.isVisible()) return;
166
+ // The `updating` record is written when the host refused a connect
167
+ // mid-update; it only changes when someone connects again. While the
168
+ // User has the Computer open, rejoin the update each poll so the
169
+ // viewer arrives the moment the host finishes (P4).
170
+ if (machine.phase === "updating" && machine.expanded) {
171
+ void execute("connect").catch(() => {
172
+ // A refusal keeps the updating phase; the next poll asks again.
173
+ });
174
+ return;
175
+ }
166
176
  void load(selectedBotId).catch((error) =>
167
177
  apply({ type: "failed", message: errorMessage(error) }),
168
178
  );
@@ -234,9 +244,18 @@ export function createComputerClientPlugin(
234
244
 
235
245
  async function openViewer(): Promise<void> {
236
246
  if (machine.expanded) return;
237
- const wake = machine.phase === "idle";
247
+ // Idle wakes. Updating rejoins: the host either hands back the viewer
248
+ // because the update has finished, or refuses again with its phase.
249
+ const wake = machine.phase === "idle" || machine.phase === "updating";
238
250
  apply({ type: "viewer-expanded" });
239
- if (wake) await connect("connect-requested");
251
+ if (!wake) return;
252
+ if (machine.phase === "updating") {
253
+ await execute("connect").catch(() => {
254
+ // Still updating; the projection poll keeps rejoining.
255
+ });
256
+ return;
257
+ }
258
+ await connect("connect-requested");
240
259
  }
241
260
 
242
261
  async function closeViewer(): Promise<void> {