@frockbot/plugin-shell 0.3.0 → 0.3.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.
Files changed (33) hide show
  1. package/package.json +32 -29
  2. package/src/backend-applets.test.ts +581 -0
  3. package/src/backend-applets.ts +959 -0
  4. package/src/backend-authoring.test.ts +61 -19
  5. package/src/backend-authoring.ts +52 -26
  6. package/src/backend-composition.ts +64 -0
  7. package/src/backend-computer.test.ts +128 -0
  8. package/src/backend-computer.ts +81 -0
  9. package/src/backend-configuration.test.ts +8 -1
  10. package/src/backend-iframe-ui.test.ts +29 -12
  11. package/src/backend-isolate.ts +31 -5
  12. package/src/backend-package-catalog.test.ts +13 -8
  13. package/src/backend-package-catalog.ts +8 -6
  14. package/src/backend-recovery-integration.test.ts +23 -0
  15. package/src/backend.ts +508 -5
  16. package/src/client/AppletCanvas.vue +679 -0
  17. package/src/client/FrockBotApp.vue +169 -15
  18. package/src/client/PackageEntryTrigger.vue +77 -0
  19. package/src/client/PackageIframeHost.vue +148 -47
  20. package/src/client/PackageIframeSettings.vue +8 -6
  21. package/src/client/PackageSurfacePage.vue +39 -0
  22. package/src/client/applets-client.test.ts +204 -0
  23. package/src/client/applets-client.ts +139 -0
  24. package/src/client/applets-state.ts +64 -0
  25. package/src/client/index.test.ts +13 -7
  26. package/src/client/index.ts +308 -1
  27. package/src/client/package-iframe-entries.test.ts +122 -0
  28. package/src/client/package-iframe-entries.ts +112 -0
  29. package/src/client/package-iframe-host-message.test.ts +3 -3
  30. package/src/client/package-iframe-host-message.ts +3 -3
  31. package/src/client/styles.css +107 -1
  32. package/src/composition-views.ts +31 -6
  33. package/src/shared.ts +52 -0
@@ -49,6 +49,15 @@
49
49
  --panel-current: var(--panel-surface-width);
50
50
  }
51
51
 
52
+ /*
53
+ * An Applet needs room a summary column does not have, and how much is the
54
+ * User's: `--applet-panel-width` is the width they last dragged, written by
55
+ * the component and remembered per browser.
56
+ */
57
+ .app-shell.panel-open.panel-applet {
58
+ --panel-current: var(--applet-panel-width, 480px);
59
+ }
60
+
52
61
  /* Sidebar */
53
62
 
54
63
  .sidebar {
@@ -577,6 +586,44 @@
577
586
  * message being composed, not a separate control.
578
587
  */
579
588
 
589
+ /*
590
+ * The focused Applet, on a phone, while its panel is closed.
591
+ *
592
+ * It sits above the composer rather than inside it: the composer's row is the
593
+ * message and the send control, and squeezing a third thing into it is what
594
+ * makes a phone composer unusable.
595
+ */
596
+ .applet-chip {
597
+ position: absolute;
598
+ bottom: calc(100% + 8px);
599
+ left: 0;
600
+ display: inline-flex;
601
+ max-width: 100%;
602
+ height: var(--frock-control-md);
603
+ align-items: center;
604
+ gap: 6px;
605
+ padding: 0 10px;
606
+ border: 1px solid var(--frock-border-strong);
607
+ border-radius: 999px;
608
+ color: var(--frock-text);
609
+ background: var(--frock-surface-raised);
610
+ box-shadow: var(--frock-shadow-control);
611
+ font-size: var(--frock-text-sm);
612
+ cursor: pointer;
613
+ }
614
+
615
+ .applet-chip-name {
616
+ overflow: hidden;
617
+ white-space: nowrap;
618
+ text-overflow: ellipsis;
619
+ }
620
+
621
+ .applet-chip-action {
622
+ flex: 0 0 auto;
623
+ color: var(--frock-action-primary);
624
+ font-weight: 600;
625
+ }
626
+
580
627
  .composer-body {
581
628
  display: flex;
582
629
  width: 100%;
@@ -702,6 +749,50 @@
702
749
  flex-direction: column;
703
750
  }
704
751
 
752
+ .panel-applet .right-panel-content {
753
+ width: var(--applet-panel-width, 480px);
754
+ }
755
+
756
+ /*
757
+ * The Applet panel's drag edge. It is 8px of grab area over a 1px seam, it
758
+ * takes focus, and the arrow keys resize by the same amounts the drag does, so
759
+ * the width is not a mouse-only setting.
760
+ */
761
+ .applet-panel-handle {
762
+ position: absolute;
763
+ z-index: 16;
764
+ top: 0;
765
+ bottom: 0;
766
+ left: -4px;
767
+ width: 8px;
768
+ border: 0;
769
+ padding: 0;
770
+ background: transparent;
771
+ cursor: col-resize;
772
+ touch-action: none;
773
+ }
774
+
775
+ .applet-panel-handle::after {
776
+ position: absolute;
777
+ top: 0;
778
+ bottom: 0;
779
+ left: 3px;
780
+ width: 2px;
781
+ background: transparent;
782
+ content: "";
783
+ transition: background-color var(--frock-motion-fast);
784
+ }
785
+
786
+ .applet-panel-handle:hover::after,
787
+ .applet-panel-handle:focus-visible::after {
788
+ background: var(--frock-action-primary);
789
+ }
790
+
791
+ .applet-panel-handle:focus-visible {
792
+ outline: 2px solid var(--frock-border-focus);
793
+ outline-offset: -2px;
794
+ }
795
+
705
796
  .right-panel-header {
706
797
  display: flex;
707
798
  height: var(--frock-titlebar-height);
@@ -788,6 +879,11 @@
788
879
  width: var(--panel-surface-width);
789
880
  }
790
881
 
882
+ .app-shell.panel-applet .right-panel {
883
+ width: var(--applet-panel-width, 480px);
884
+ max-width: 100%;
885
+ }
886
+
791
887
  /*
792
888
  * A parked panel is hidden as well as off-canvas: it is not something to
793
889
  * tab into, read out, or hit-test, and `visibility` is the one property that
@@ -894,15 +990,25 @@
894
990
 
895
991
  /* The right panel is a drawer too, and on a phone it takes the whole width. */
896
992
  .right-panel,
897
- .app-shell.panel-surface .right-panel {
993
+ .app-shell.panel-surface .right-panel,
994
+ .app-shell.panel-applet .right-panel {
898
995
  width: 100%;
899
996
  }
900
997
 
901
998
  .right-panel-content,
999
+ .panel-applet .right-panel-content,
902
1000
  .panel-surface-view {
903
1001
  width: 100%;
904
1002
  }
905
1003
 
1004
+ /*
1005
+ * A focused Applet on a phone is a full-height sheet, not a wider column:
1006
+ * there is one column, so the canvas is the whole of it while it is open.
1007
+ */
1008
+ .app-shell.panel-open.panel-applet {
1009
+ --applet-panel-width: 100%;
1010
+ }
1011
+
906
1012
  /* There is no window chrome to drag and no traffic lights to clear. */
907
1013
  .brand {
908
1014
  padding-left: 0;
@@ -39,7 +39,26 @@ export type CompositionMemberManifestReaderV1 = (
39
39
  member: CompositionMemberV1,
40
40
  ) => Promise<FrockBotManifest | undefined>;
41
41
 
42
- /** Project iframe Contributions from either Bot or Catalog provenance. */
42
+ /** What the shell attributes an iframe page to, by where its code came from. */
43
+ function iframeProvenanceV1(
44
+ member: CompositionMemberV1,
45
+ ): PackageIframeCompositionV1["contributions"][number]["provenance"] {
46
+ if (member.provenance.kind === "bot") return "Bot-authored";
47
+ if (member.provenance.kind === "first-party") return "FrockBot";
48
+ return "User-installed";
49
+ }
50
+
51
+ /**
52
+ * Project iframe Contributions from every artifact-backed member.
53
+ *
54
+ * The test is the artifact, not the provenance. "Every Contribution kind is
55
+ * resolved from the manifest and an artifact, never from a switch over Package
56
+ * identity" — a first-party Package that ships as an artifact-backed member
57
+ * (ADR 0022 decision 8) has a manifest the Bot object holds and pages the
58
+ * anonymous origin can serve, so there is nothing left for a provenance test
59
+ * to decide. A first-party member with no artifact is in-process code whose
60
+ * client Contribution is a compiled module, and it has no iframe page to show.
61
+ */
43
62
  export async function projectPackageIframeCompositionV1(input: {
44
63
  botId: string;
45
64
  generation: CompositionGenerationV1;
@@ -47,7 +66,7 @@ export async function projectPackageIframeCompositionV1(input: {
47
66
  }): Promise<PackageIframeCompositionV1> {
48
67
  const contributions: PackageIframeCompositionV1["contributions"] = [];
49
68
  for (const member of input.generation.members) {
50
- if (member.provenance.kind === "first-party") continue;
69
+ if (member.artifact === undefined) continue;
51
70
  const manifest = await input.readMemberManifest(member);
52
71
  if (!manifest) continue;
53
72
  const client = manifest.contributions.client;
@@ -55,10 +74,16 @@ export async function projectPackageIframeCompositionV1(input: {
55
74
  contributions.push({
56
75
  packageId: member.packageId,
57
76
  displayName: manifest.displayName,
58
- provenance:
59
- member.provenance.kind === "bot" ? "Bot-authored" : "User-installed",
60
- artifact: { ...client.artifact },
61
- mounts: client.mounts.map((mount) => ({ ...mount })),
77
+ provenance: iframeProvenanceV1(member),
78
+ pages: client.pages.map((page) => ({
79
+ id: page.id,
80
+ artifact: { ...page.artifact },
81
+ mounts: page.mounts.map((mount) => ({ ...mount })),
82
+ })),
83
+ entries: (client.entries ?? []).map((entry) => ({
84
+ ...entry,
85
+ opens: { ...entry.opens },
86
+ })),
62
87
  declaredTools: (manifest.tools ?? []).map((tool) => tool.name),
63
88
  });
64
89
  }
package/src/shared.ts CHANGED
@@ -14,6 +14,9 @@ import type {
14
14
  CatalogIndexEntryV1,
15
15
  } from "@frockbot/catalog-core";
16
16
  import type {
17
+ AppletBuildViewV1,
18
+ AppletSourceViewV1,
19
+ AppletSummaryV1,
17
20
  PackageIframeCatalogV1,
18
21
  PackageIframeContributionViewV1,
19
22
  SendToUserPayloadV1,
@@ -193,6 +196,39 @@ export interface FrockBotWebData {
193
196
  tasks: TaskViewV1[];
194
197
  /** Sandboxed pages in the selected Bot's active fail-closed Composition. */
195
198
  packageUi?: PackageIframeCatalogV1;
199
+ /**
200
+ * The Applets this User owns. Account-wide, like every other Package-shaped
201
+ * capability: an Applet belongs to the User, and every Bot they own sees it.
202
+ */
203
+ applets: AppletSummaryV1[];
204
+ /**
205
+ * The Session's focused Applet id, as the Bot Durable Object recorded it.
206
+ * `null` is a value — the Session deliberately has no Applet in the canvas —
207
+ * and `undefined` means it has not been read yet.
208
+ */
209
+ focusedAppletId?: string | null;
210
+ /** The focused Applet joined with the list. Derived; never written to. */
211
+ readonly focusedApplet?: AppletSummaryV1;
212
+ /** Where the focused Applet's live UI is, and the credential to open it. */
213
+ appletViewer?: {
214
+ appletId: string;
215
+ token: string;
216
+ expiresAt: string;
217
+ socketUrl: string;
218
+ uiUrl: string;
219
+ generationId: string;
220
+ };
221
+ /** The focused Applet's source, for the canvas's building state. */
222
+ appletSource?: AppletSourceViewV1;
223
+ /** The outcome the Bot last recorded for `applet check` or `applet build`. */
224
+ appletBuild?: AppletBuildViewV1;
225
+ /**
226
+ * How the canvas's own read went. `loading` is the first read of a focused
227
+ * Applet, `failed` is a read the User can retry, and `ready` says the canvas
228
+ * has everything it needs to draw either of its states.
229
+ */
230
+ appletCanvas: "idle" | "loading" | "ready" | "failed";
231
+ appletCanvasError?: string;
196
232
  /**
197
233
  * The User's MCP servers: state, tool count, last handshake, instructions,
198
234
  * failure, and the durable refusal ledger. Absent until it is loaded, and
@@ -247,6 +283,22 @@ export interface FrockBotWebData {
247
283
  /** Refreshes {@link FrockBotWebData.tasks} for the active Bot. */
248
284
  loadTasks(): Promise<void>;
249
285
  loadPackageUi(): Promise<void>;
286
+ /** Refreshes {@link FrockBotWebData.applets} for the signed-in User. */
287
+ loadApplets(): Promise<void>;
288
+ /** Refreshes the Session's focused Applet for the active Bot. */
289
+ loadFocusedApplet(): Promise<void>;
290
+ /**
291
+ * Focuses one Applet for this Session, or clears the focus. The backend is
292
+ * the authority: what the canvas then shows is the focus that was recorded,
293
+ * never the one the click asked for.
294
+ */
295
+ setFocusedApplet(appletId: string | null): Promise<void>;
296
+ /**
297
+ * Re-reads what the canvas draws for the focused Applet: its source, the
298
+ * last recorded check or build outcome, and a viewer credential once a
299
+ * generation is active.
300
+ */
301
+ refreshAppletCanvas(): Promise<void>;
250
302
  callPackageUiTool(
251
303
  contribution: PackageIframeContributionViewV1,
252
304
  name: string,