@frockbot/plugin-shell 0.3.11 → 0.3.13

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.
Files changed (42) hide show
  1. package/package.json +35 -33
  2. package/src/agent.test.ts +78 -0
  3. package/src/agent.ts +130 -2
  4. package/src/backend-configuration.test.ts +26 -26
  5. package/src/backend-recovery-integration.test.ts +10 -10
  6. package/src/backend-runner.ts +19 -2
  7. package/src/backend.ts +85 -18
  8. package/src/client/AppletCanvas.vue +19 -6
  9. package/src/client/FrockBotApp.vue +405 -75
  10. package/src/client/activity-trail.test.ts +205 -0
  11. package/src/client/activity-trail.ts +227 -0
  12. package/src/client/applets-client.test.ts +62 -0
  13. package/src/client/applets-client.ts +19 -0
  14. package/src/client/index.test.ts +128 -21
  15. package/src/client/index.ts +359 -114
  16. package/src/client/model-presentation.test.ts +3 -3
  17. package/src/client/no-bot-model-label.test.ts +7 -7
  18. package/src/client/skill-invocation.test.ts +34 -0
  19. package/src/client/skill-invocation.ts +22 -0
  20. package/src/client/styles.css +69 -19
  21. package/src/client/transcript-cache.test.ts +125 -0
  22. package/src/client/transcript-cache.ts +190 -0
  23. package/src/compaction-scheduler.test.ts +96 -0
  24. package/src/compaction-scheduler.ts +108 -0
  25. package/src/compaction-transcript.test.ts +174 -0
  26. package/src/compaction.test.ts +596 -0
  27. package/src/compaction.ts +539 -0
  28. package/src/focus.test.ts +222 -0
  29. package/src/focus.ts +93 -0
  30. package/src/history.ts +86 -8
  31. package/src/legacy-frock-model-id.test.ts +148 -0
  32. package/src/notification-id.ts +0 -0
  33. package/src/run-failure-copy.test.ts +150 -0
  34. package/src/run-failure-copy.ts +110 -0
  35. package/src/run-protocol.test.ts +50 -7
  36. package/src/run-protocol.ts +152 -43
  37. package/src/settings-links.test.ts +8 -2
  38. package/src/settings-links.ts +11 -2
  39. package/src/shared.ts +36 -0
  40. package/tsconfig.json +1 -2
  41. package/src/client/activity-ring.test.ts +0 -89
  42. package/src/client/activity-ring.ts +0 -94
@@ -572,7 +572,11 @@ describe("Bot selection", () => {
572
572
  await Promise.all([botLoad, olderLoad]);
573
573
 
574
574
  expect(provided.value.userSettings?.profile.name).toBe("Newer");
575
- expect(provided.value.settingsError).toBe("Bot settings unavailable");
575
+ // The surface says what failed and what it was for; the thrown text
576
+ // ("Bot settings unavailable") stays in the console.
577
+ expect(provided.value.settingsError).toBe(
578
+ "Couldn't load this Bot's settings.",
579
+ );
576
580
  });
577
581
 
578
582
  test("says a deployment has no Catalog instead of relaying the gateway's sentence", async () => {
@@ -608,12 +612,14 @@ describe("Bot selection", () => {
608
612
  "No plugins are published for this deployment yet.",
609
613
  );
610
614
 
611
- // A genuine fault still reaches the operator, labelled as one.
615
+ // A genuine fault is labelled as one and says what failed rather than
616
+ // what the storage layer called it. "R2 is down" is for the console.
612
617
  failure = new Error("R2 is down");
613
618
  await provided.value.loadPackageCatalog();
614
619
  expect(provided.value.settingsError).toBe(
615
- "Plugins could not be loaded: R2 is down",
620
+ "Couldn't load the plugin catalog.",
616
621
  );
622
+ expect(provided.value.settingsError).not.toContain("R2");
617
623
  });
618
624
 
619
625
  test("commits a catalog without overwriting newer User settings", async () => {
@@ -1258,14 +1264,20 @@ describe("detached Turn projection", () => {
1258
1264
  ],
1259
1265
  );
1260
1266
 
1267
+ // The failure is a notice under the reply, never a bubble that reads as
1268
+ // the Bot saying the provider's own words.
1261
1269
  expect(messages).toMatchObject([
1262
1270
  { role: "user", status: "completed" },
1263
1271
  {
1264
1272
  role: "assistant",
1265
- text: "This Bot couldn't finish its reply. Try again.",
1273
+ text: "",
1274
+ notice: "This Bot couldn't finish its reply. Try again.",
1266
1275
  status: "error",
1267
1276
  },
1268
1277
  ]);
1278
+ expect(JSON.stringify(messages)).not.toContain(
1279
+ "Provider reconciliation is required",
1280
+ );
1269
1281
  });
1270
1282
  });
1271
1283
 
@@ -1514,16 +1526,95 @@ describe("active durable Turn projection", () => {
1514
1526
  ],
1515
1527
  );
1516
1528
 
1517
- expect(state.messages[1]?.sends).toEqual([
1529
+ // One message per send, in the order they were sent, and nothing merged:
1530
+ // the last line is the Turn's own, and it carries no send at all.
1531
+ expect(
1532
+ state.messages.slice(1).map((message) => [message.id, message.sends]),
1533
+ ).toEqual([
1534
+ [
1535
+ "run-send:send:0",
1536
+ [{ kind: "payload", payload: { type: "text", text: "On it." } }],
1537
+ ],
1538
+ [
1539
+ "run-send:send:1",
1540
+ [
1541
+ {
1542
+ kind: "payload",
1543
+ payload: {
1544
+ type: "widget",
1545
+ widget: { prompt: "Which day?", options: ["Tue"] },
1546
+ },
1547
+ },
1548
+ ],
1549
+ ],
1550
+ ["run-send:send:2", [{ kind: "unsupported" }]],
1551
+ ["run-send:assistant", []],
1552
+ ]);
1553
+ });
1554
+
1555
+ test("a second send is a new bubble, and never rewrites the first", () => {
1556
+ const state: Pick<
1557
+ FrockBotWebData,
1558
+ "messages" | "activeRunId" | "activeRun"
1559
+ > = { messages: [] };
1560
+ const run = (
1561
+ events: ClientRun["events"],
1562
+ status: ClientRun["status"] = "running",
1563
+ ): ClientRun => ({
1564
+ runId: "run-stack",
1565
+ input: "Book it",
1566
+ admittedAt: "2026-09-04T00:00:00.000Z",
1567
+ status,
1568
+ events,
1569
+ });
1570
+
1571
+ // The acknowledgement arrives first, on its own.
1572
+ projectDurableRuns(state, [], [
1573
+ run([
1574
+ { type: "send/to-user", payload: { type: "text", text: "On it." } },
1575
+ ]),
1576
+ ] as ClientRun[]);
1577
+ const acknowledgement = state.messages[1];
1578
+ expect(acknowledgement?.sends).toEqual([
1518
1579
  { kind: "payload", payload: { type: "text", text: "On it." } },
1519
- {
1520
- kind: "payload",
1521
- payload: {
1522
- type: "widget",
1523
- widget: { prompt: "Which day?", options: ["Tue"] },
1524
- },
1525
- },
1526
- { kind: "unsupported" },
1580
+ ]);
1581
+
1582
+ // The result follows in the same Turn. It appends: the first bubble is
1583
+ // still the first bubble, still saying what it said.
1584
+ projectDurableRuns(state, [], [
1585
+ run(
1586
+ [
1587
+ {
1588
+ type: "send/to-user",
1589
+ payload: { type: "text", text: "On it." },
1590
+ },
1591
+ { type: "send/to-user", payload: { type: "text", text: "Booked." } },
1592
+ ],
1593
+ "completed",
1594
+ ),
1595
+ ] as ClientRun[]);
1596
+
1597
+ expect(
1598
+ state.messages
1599
+ .filter((message) => message.sends.length > 0)
1600
+ .map((message) => [message.id, message.sends]),
1601
+ ).toEqual([
1602
+ [
1603
+ "run-stack:send:0",
1604
+ [{ kind: "payload", payload: { type: "text", text: "On it." } }],
1605
+ ],
1606
+ [
1607
+ "run-stack:send:1",
1608
+ [{ kind: "payload", payload: { type: "text", text: "Booked." } }],
1609
+ ],
1610
+ ]);
1611
+ // The Turn's lines stay together and in order behind the prompt that
1612
+ // started it, so a reload draws the thread that was watched being written.
1613
+ expect(state.messages.map((message) => message.id)).toEqual([
1614
+ "run-stack:user",
1615
+ "run-stack:send:0",
1616
+ "run-stack:send:1",
1617
+ "run-stack:assistant",
1527
1618
  ]);
1528
1619
  });
1529
1620
 
@@ -1549,8 +1640,8 @@ describe("active durable Turn projection", () => {
1549
1640
  ],
1550
1641
  );
1551
1642
 
1552
- expect(state.messages[1]?.text).toBe("");
1553
1643
  expect(state.messages[1]?.sends).toHaveLength(1);
1644
+ expect(state.messages[2]?.text).toBe("");
1554
1645
  });
1555
1646
 
1556
1647
  test("a Turn that sent nothing still draws the model's text", () => {
@@ -1700,8 +1791,8 @@ describe("active durable Turn projection", () => {
1700
1791
  },
1701
1792
  ],
1702
1793
  );
1703
- expect(state.messages[1]).toMatchObject({ text: "", status: "streaming" });
1704
1794
  expect(state.messages[1]?.sends).toHaveLength(1);
1795
+ expect(state.messages[2]).toMatchObject({ text: "", status: "streaming" });
1705
1796
  });
1706
1797
 
1707
1798
  test("projects reconciliation-required recovery state", () => {
@@ -1726,6 +1817,9 @@ describe("active durable Turn projection", () => {
1726
1817
  },
1727
1818
  ],
1728
1819
  );
1820
+ // One sentence of the product's own, and the raw provider text nowhere on
1821
+ // screen — the banner, the bubble and the notice all read the same way
1822
+ // whatever the provider called the failure.
1729
1823
  expect(reconciliation.activeRun).toEqual({
1730
1824
  runId: "run-reconciliation",
1731
1825
  status: "reconciliation-required",
@@ -1733,9 +1827,14 @@ describe("active durable Turn projection", () => {
1733
1827
  canResume: true,
1734
1828
  });
1735
1829
  expect(reconciliation.messages[1]).toMatchObject({
1736
- text: "This reply stopped partway. Try again to continue it.",
1830
+ role: "assistant",
1831
+ text: "",
1832
+ notice: "This reply stopped partway. Try again to continue it.",
1737
1833
  status: "reconciliation-required",
1738
1834
  });
1835
+ expect(JSON.stringify(reconciliation.messages)).not.toContain(
1836
+ "Provider result needs confirmation",
1837
+ );
1739
1838
  });
1740
1839
 
1741
1840
  test("keeps busy state until the durable run becomes terminal", () => {
@@ -3195,11 +3294,14 @@ describe("a message sent while a Turn is running", () => {
3195
3294
  expect(state.messages.every((message) => !message.pending)).toBe(true);
3196
3295
  expect(state.activeRunId).toBe("run-2");
3197
3296
  expect(state.runningRunId).toBe("run-2");
3198
- // The superseded Turn keeps the quiet treatment a stopped one gets.
3199
- expect(state.messages[1]).toMatchObject({
3200
- status: "aborted",
3201
- notice: "Interrupted by your next message.",
3202
- });
3297
+ // The superseded Turn is left unlabelled: the message that replaced it is
3298
+ // right underneath, and it says why the Turn ends there better than a
3299
+ // notice of ours would.
3300
+ expect(state.messages[1]).toMatchObject({ status: "aborted" });
3301
+ expect(state.messages[1]?.notice).toBeUndefined();
3302
+ expect(state.messages.some((message) => message.notice !== undefined)).toBe(
3303
+ false,
3304
+ );
3203
3305
  });
3204
3306
 
3205
3307
  test("a reload reconstructs the greyed state from durable runs alone", () => {
@@ -3234,6 +3336,11 @@ describe("a message sent while a Turn is running", () => {
3234
3336
  text: "second",
3235
3337
  pending: true,
3236
3338
  });
3339
+ // The durable record still says why the Turn ended; the transcript does
3340
+ // not repeat it at the reader.
3341
+ expect(
3342
+ reloaded.messages.some((message) => message.notice !== undefined),
3343
+ ).toBe(false);
3237
3344
  expect(reloaded.runningRunId).toBeUndefined();
3238
3345
  expect(reloaded.activeRunId).toBe("run-2");
3239
3346
  });