@frockbot/plugin-shell 0.3.1 → 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.
- package/package.json +32 -29
- package/src/backend-applets.test.ts +581 -0
- package/src/backend-applets.ts +959 -0
- package/src/backend-authoring.test.ts +61 -19
- package/src/backend-authoring.ts +52 -26
- package/src/backend-composition.ts +64 -0
- package/src/backend-computer.test.ts +128 -0
- package/src/backend-computer.ts +81 -0
- package/src/backend-configuration.test.ts +8 -1
- package/src/backend-iframe-ui.test.ts +29 -12
- package/src/backend-isolate.ts +31 -5
- package/src/backend-package-catalog.test.ts +13 -8
- package/src/backend-package-catalog.ts +8 -6
- package/src/backend-recovery-integration.test.ts +23 -0
- package/src/backend.ts +508 -5
- package/src/client/AppletCanvas.vue +679 -0
- package/src/client/FrockBotApp.vue +169 -15
- package/src/client/PackageEntryTrigger.vue +77 -0
- package/src/client/PackageIframeHost.vue +148 -47
- package/src/client/PackageIframeSettings.vue +8 -6
- package/src/client/PackageSurfacePage.vue +39 -0
- package/src/client/applets-client.test.ts +204 -0
- package/src/client/applets-client.ts +139 -0
- package/src/client/applets-state.ts +64 -0
- package/src/client/index.test.ts +13 -7
- package/src/client/index.ts +308 -1
- package/src/client/package-iframe-entries.test.ts +122 -0
- package/src/client/package-iframe-entries.ts +112 -0
- package/src/client/package-iframe-host-message.test.ts +3 -3
- package/src/client/package-iframe-host-message.ts +3 -3
- package/src/client/styles.css +107 -1
- package/src/composition-views.ts +31 -6
- package/src/shared.ts +52 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A Package page hosted as a surface.
|
|
4
|
+
*
|
|
5
|
+
* The surface's chrome — its title and the way out of it — belongs to the
|
|
6
|
+
* shell; the page fills the body and is attributed to the Package that ships
|
|
7
|
+
* it, so a User always knows whose screen they are looking at.
|
|
8
|
+
*/
|
|
9
|
+
import { computed, inject } from "vue";
|
|
10
|
+
import { frockBotWebDataKey } from "../shared.js";
|
|
11
|
+
import { appletsBridgeStateV2 } from "./applets-state.js";
|
|
12
|
+
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
13
|
+
import type { PackageIframeEntryV1 } from "./package-iframe-entries.js";
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{ entry: PackageIframeEntryV1 }>();
|
|
16
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
17
|
+
if (!providedWeb) throw new Error("Package surface data was not provided");
|
|
18
|
+
const web = providedWeb;
|
|
19
|
+
const states = computed(() => ({ applets: appletsBridgeStateV2(web.value) }));
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="package-surface-page">
|
|
24
|
+
<PackageIframeHost
|
|
25
|
+
:contribution="props.entry.contribution"
|
|
26
|
+
:page="props.entry.page"
|
|
27
|
+
:slot="props.entry.slot"
|
|
28
|
+
:states="states"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
.package-surface-page {
|
|
35
|
+
display: grid;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
gap: 16px;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
mostRecentlyChangedFileV1,
|
|
4
|
+
readAppletList,
|
|
5
|
+
readAppletSource,
|
|
6
|
+
readFocusedAppletId,
|
|
7
|
+
writeFocusedAppletId,
|
|
8
|
+
type AppletsHostedRequest,
|
|
9
|
+
} from "./applets-client.js";
|
|
10
|
+
import { appletsAvailableV1, appletsBridgeStateV2 } from "./applets-state.js";
|
|
11
|
+
import type { FrockBotWebData } from "../shared.js";
|
|
12
|
+
|
|
13
|
+
const summary = {
|
|
14
|
+
appletId: "u1abc.todo",
|
|
15
|
+
displayName: "Todo",
|
|
16
|
+
status: "published" as const,
|
|
17
|
+
currentGenerationId: "generation-2",
|
|
18
|
+
tools: ["add_todo"],
|
|
19
|
+
createdAt: "2026-09-03T00:00:00.000Z",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** A fake transport, so the client is proven without a backend on the branch. */
|
|
23
|
+
function transportOf(
|
|
24
|
+
answers: Record<string, unknown>,
|
|
25
|
+
recorded: Array<{ path: string; method?: string; body?: string }> = [],
|
|
26
|
+
): AppletsHostedRequest {
|
|
27
|
+
return (path, method, body) => {
|
|
28
|
+
recorded.push({
|
|
29
|
+
path,
|
|
30
|
+
...(method ? { method } : {}),
|
|
31
|
+
...(body ? { body } : {}),
|
|
32
|
+
});
|
|
33
|
+
if (!(path in answers))
|
|
34
|
+
return Promise.reject(new Error(`no route ${path}`));
|
|
35
|
+
return Promise.resolve(answers[path]);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("Applet routes", () => {
|
|
40
|
+
test("reads the User's Applets through the strict decoder", async () => {
|
|
41
|
+
const request = transportOf({
|
|
42
|
+
"/api/applets": { schemaVersion: 1, applets: [summary] },
|
|
43
|
+
});
|
|
44
|
+
expect(await readAppletList(request)).toEqual([summary]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("refuses a list the decoder does not accept", () => {
|
|
48
|
+
const request = transportOf({ "/api/applets": { applets: [summary] } });
|
|
49
|
+
expect(readAppletList(request)).rejects.toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the focus write reads back what the backend recorded", async () => {
|
|
53
|
+
const recorded: Array<{ path: string; method?: string; body?: string }> =
|
|
54
|
+
[];
|
|
55
|
+
const request = transportOf(
|
|
56
|
+
{ "/api/bots/bot-1/applets/focus": { appletId: "u1abc.todo" } },
|
|
57
|
+
recorded,
|
|
58
|
+
);
|
|
59
|
+
// The click asked for one Applet; the answer is the one that was recorded.
|
|
60
|
+
expect(await writeFocusedAppletId(request, "bot-1", "u1abc.other")).toBe(
|
|
61
|
+
"u1abc.todo",
|
|
62
|
+
);
|
|
63
|
+
expect(recorded[0]).toEqual({
|
|
64
|
+
path: "/api/bots/bot-1/applets/focus",
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify({ schemaVersion: 1, appletId: "u1abc.other" }),
|
|
67
|
+
});
|
|
68
|
+
expect(await readFocusedAppletId(request, "bot-1")).toBe("u1abc.todo");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("no focus is a value, not a missing read", async () => {
|
|
72
|
+
const request = transportOf({
|
|
73
|
+
"/api/bots/bot-1/applets/focus": { appletId: null },
|
|
74
|
+
});
|
|
75
|
+
expect(await readFocusedAppletId(request, "bot-1")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("the source read is bot-scoped and decoded", async () => {
|
|
79
|
+
const view = {
|
|
80
|
+
appletId: "u1abc.todo",
|
|
81
|
+
files: [
|
|
82
|
+
{
|
|
83
|
+
path: "server.ts",
|
|
84
|
+
text: "export class A {}",
|
|
85
|
+
generationId: "w-1",
|
|
86
|
+
changedAt: "2026-09-03T00:01:00.000Z",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
truncated: false,
|
|
90
|
+
};
|
|
91
|
+
const request = transportOf({
|
|
92
|
+
"/api/bots/bot-1/applets/u1abc.todo/source": view,
|
|
93
|
+
});
|
|
94
|
+
expect(await readAppletSource(request, "bot-1", "u1abc.todo")).toEqual(
|
|
95
|
+
view,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the code view follows the most recently changed file", () => {
|
|
100
|
+
expect(mostRecentlyChangedFileV1(undefined)).toBeUndefined();
|
|
101
|
+
expect(
|
|
102
|
+
mostRecentlyChangedFileV1({
|
|
103
|
+
appletId: "u1abc.todo",
|
|
104
|
+
truncated: false,
|
|
105
|
+
files: [
|
|
106
|
+
{
|
|
107
|
+
path: "server.ts",
|
|
108
|
+
text: "",
|
|
109
|
+
generationId: "w-1",
|
|
110
|
+
changedAt: "2026-09-03T00:01:00.000Z",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
path: "ui.tsx",
|
|
114
|
+
text: "",
|
|
115
|
+
generationId: "w-2",
|
|
116
|
+
changedAt: "2026-09-03T00:05:00.000Z",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
).toBe("ui.tsx");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("the applets feed a page receives", () => {
|
|
125
|
+
const web = {
|
|
126
|
+
applets: [summary],
|
|
127
|
+
focusedAppletId: "u1abc.todo",
|
|
128
|
+
appletViewer: {
|
|
129
|
+
appletId: "u1abc.todo",
|
|
130
|
+
token: "signed",
|
|
131
|
+
expiresAt: "2026-09-03T00:15:00.000Z",
|
|
132
|
+
socketUrl: "wss://bot.example.com/api/applets/u1abc.todo/socket",
|
|
133
|
+
uiUrl: "https://ui.example.com/packages/aa.html",
|
|
134
|
+
generationId: "generation-2",
|
|
135
|
+
},
|
|
136
|
+
appletSource: undefined,
|
|
137
|
+
appletBuild: undefined,
|
|
138
|
+
} satisfies Pick<
|
|
139
|
+
FrockBotWebData,
|
|
140
|
+
| "applets"
|
|
141
|
+
| "focusedAppletId"
|
|
142
|
+
| "appletViewer"
|
|
143
|
+
| "appletSource"
|
|
144
|
+
| "appletBuild"
|
|
145
|
+
>;
|
|
146
|
+
|
|
147
|
+
test("carries the focused Applet, the list, and the viewer", () => {
|
|
148
|
+
const state = appletsBridgeStateV2(web);
|
|
149
|
+
expect(state.focused).toEqual(summary);
|
|
150
|
+
expect(state.list).toEqual([summary]);
|
|
151
|
+
expect(state.viewer?.generationId).toBe("generation-2");
|
|
152
|
+
expect(state.viewer).not.toHaveProperty("expiresAt");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a viewer credential minted for another Applet is not handed over", () => {
|
|
156
|
+
const state = appletsBridgeStateV2({
|
|
157
|
+
...web,
|
|
158
|
+
focusedAppletId: "u1abc.other",
|
|
159
|
+
});
|
|
160
|
+
expect(state.viewer).toBeNull();
|
|
161
|
+
expect(state.focused).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("Applets are available only when a Package declares the focus tool", () => {
|
|
165
|
+
const page = {
|
|
166
|
+
id: "canvas",
|
|
167
|
+
artifact: {
|
|
168
|
+
contentHash: "a".repeat(64),
|
|
169
|
+
size: 1,
|
|
170
|
+
mediaType: "text/html" as const,
|
|
171
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
172
|
+
},
|
|
173
|
+
mounts: [{ slot: "frockbot.right-panel" }],
|
|
174
|
+
};
|
|
175
|
+
const catalog = (declaredTools: string[]) => ({
|
|
176
|
+
schemaVersion: 1 as const,
|
|
177
|
+
botId: "bot-1",
|
|
178
|
+
generationId: "generation-1",
|
|
179
|
+
artifactOrigin: "https://ui.example.com",
|
|
180
|
+
contributions: [
|
|
181
|
+
{
|
|
182
|
+
packageId: "applets",
|
|
183
|
+
displayName: "Applets",
|
|
184
|
+
provenance: "Bot-authored" as const,
|
|
185
|
+
pages: [page],
|
|
186
|
+
entries: [],
|
|
187
|
+
declaredTools,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
// Derived from a manifest fact, never from a Package id: a deployment
|
|
192
|
+
// without Applets asks for no Applet route at all.
|
|
193
|
+
expect(appletsAvailableV1(catalog(["applet_focus"]))).toBe(true);
|
|
194
|
+
expect(appletsAvailableV1(catalog(["weather_lookup"]))).toBe(false);
|
|
195
|
+
expect(appletsAvailableV1(undefined)).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("no focus is null rather than an absent field", () => {
|
|
199
|
+
const state = appletsBridgeStateV2({ ...web, focusedAppletId: null });
|
|
200
|
+
expect(state.focused).toBeNull();
|
|
201
|
+
expect(state.viewer).toBeNull();
|
|
202
|
+
expect(state.list).toEqual([summary]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client half of the Applet routes.
|
|
3
|
+
*
|
|
4
|
+
* The backend is the authority for every one of these reads; this module is
|
|
5
|
+
* only the typed seam between the hosted transport and the shell's projection,
|
|
6
|
+
* so a route that is absent from a deployment reads as "no Applets" rather
|
|
7
|
+
* than as an error over the User's conversation. Every response crosses a
|
|
8
|
+
* strict decoder before it reaches the shell.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
decodeAppletBuildViewV1,
|
|
12
|
+
decodeAppletFocusViewV1,
|
|
13
|
+
decodeAppletListViewV1,
|
|
14
|
+
decodeAppletSourceViewV1,
|
|
15
|
+
decodeAppletUiViewV1,
|
|
16
|
+
decodeAppletViewerTokenV1,
|
|
17
|
+
type AppletBuildViewV1,
|
|
18
|
+
type AppletSourceViewV1,
|
|
19
|
+
type AppletSummaryV1,
|
|
20
|
+
type AppletUiViewV1,
|
|
21
|
+
type AppletViewerTokenV1,
|
|
22
|
+
} from "@frockbot/kernel-contracts";
|
|
23
|
+
|
|
24
|
+
/** Exactly what the shell's hosted transport offers, and nothing more. */
|
|
25
|
+
export type AppletsHostedRequest = (
|
|
26
|
+
path: string,
|
|
27
|
+
method?: "GET" | "POST",
|
|
28
|
+
body?: string,
|
|
29
|
+
) => Promise<unknown>;
|
|
30
|
+
|
|
31
|
+
const applet = (appletId: string) => encodeURIComponent(appletId);
|
|
32
|
+
|
|
33
|
+
export async function readAppletList(
|
|
34
|
+
request: AppletsHostedRequest,
|
|
35
|
+
): Promise<AppletSummaryV1[]> {
|
|
36
|
+
return decodeAppletListViewV1(await request("/api/applets")).applets;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readAppletViewerToken(
|
|
40
|
+
request: AppletsHostedRequest,
|
|
41
|
+
appletId: string,
|
|
42
|
+
): Promise<AppletViewerTokenV1> {
|
|
43
|
+
return decodeAppletViewerTokenV1(
|
|
44
|
+
await request(`/api/applets/${applet(appletId)}/token`),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readAppletUi(
|
|
49
|
+
request: AppletsHostedRequest,
|
|
50
|
+
appletId: string,
|
|
51
|
+
): Promise<AppletUiViewV1> {
|
|
52
|
+
return decodeAppletUiViewV1(
|
|
53
|
+
await request(`/api/applets/${applet(appletId)}/ui`),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* The canvas's two Workspace-backed reads are Bot-scoped in the URL and
|
|
59
|
+
* User-scoped in what they answer: the Applets root belongs to the User, and
|
|
60
|
+
* the Bot in the path only names the Durable Object that holds the Workspace
|
|
61
|
+
* binding. Reading them wakes no Computer.
|
|
62
|
+
*/
|
|
63
|
+
export async function readAppletSource(
|
|
64
|
+
request: AppletsHostedRequest,
|
|
65
|
+
botId: string,
|
|
66
|
+
appletId: string,
|
|
67
|
+
): Promise<AppletSourceViewV1> {
|
|
68
|
+
return decodeAppletSourceViewV1(
|
|
69
|
+
await request(
|
|
70
|
+
`/api/bots/${encodeURIComponent(botId)}/applets/${applet(appletId)}/source`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readAppletBuild(
|
|
76
|
+
request: AppletsHostedRequest,
|
|
77
|
+
botId: string,
|
|
78
|
+
appletId: string,
|
|
79
|
+
): Promise<AppletBuildViewV1> {
|
|
80
|
+
return decodeAppletBuildViewV1(
|
|
81
|
+
await request(
|
|
82
|
+
`/api/bots/${encodeURIComponent(botId)}/applets/${applet(appletId)}/build`,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function readFocusedAppletId(
|
|
88
|
+
request: AppletsHostedRequest,
|
|
89
|
+
botId: string,
|
|
90
|
+
): Promise<string | null> {
|
|
91
|
+
return decodeAppletFocusViewV1(
|
|
92
|
+
await request(`/api/bots/${encodeURIComponent(botId)}/applets/focus`),
|
|
93
|
+
).appletId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Records the Session's focused Applet and returns what the backend recorded,
|
|
98
|
+
* never what the click asked for: a focus the backend refused reads back as
|
|
99
|
+
* the focus it kept.
|
|
100
|
+
*/
|
|
101
|
+
export async function writeFocusedAppletId(
|
|
102
|
+
request: AppletsHostedRequest,
|
|
103
|
+
botId: string,
|
|
104
|
+
appletId: string | null,
|
|
105
|
+
): Promise<string | null> {
|
|
106
|
+
return decodeAppletFocusViewV1(
|
|
107
|
+
await request(
|
|
108
|
+
`/api/bots/${encodeURIComponent(botId)}/applets/focus`,
|
|
109
|
+
"POST",
|
|
110
|
+
JSON.stringify({ schemaVersion: 1, appletId }),
|
|
111
|
+
),
|
|
112
|
+
).appletId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The file the canvas shows while a Bot is writing an Applet: the one that
|
|
117
|
+
* changed most recently, falling back to the first path in sorted order so a
|
|
118
|
+
* store with no timestamps still opens on something.
|
|
119
|
+
*/
|
|
120
|
+
export function mostRecentlyChangedFileV1(
|
|
121
|
+
source: AppletSourceViewV1 | undefined,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
if (!source || source.files.length === 0) return undefined;
|
|
124
|
+
// A tie on time — a fresh scaffold, one sync — opens on the file a Bot edits
|
|
125
|
+
// first, not on the README the alphabet would pick.
|
|
126
|
+
const preferred = ["server.ts", "ui.tsx"];
|
|
127
|
+
const ordered = source.files.toSorted((left, right) => {
|
|
128
|
+
const leftAt = left.changedAt ?? "";
|
|
129
|
+
const rightAt = right.changedAt ?? "";
|
|
130
|
+
if (leftAt !== rightAt) return rightAt.localeCompare(leftAt);
|
|
131
|
+
const leftRank = preferred.indexOf(left.path);
|
|
132
|
+
const rightRank = preferred.indexOf(right.path);
|
|
133
|
+
const rank = (value: number) => (value < 0 ? preferred.length : value);
|
|
134
|
+
return (
|
|
135
|
+
rank(leftRank) - rank(rightRank) || left.path.localeCompare(right.path)
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
return ordered[0]?.path;
|
|
139
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `applets` feed a bridge v2 page receives.
|
|
3
|
+
*
|
|
4
|
+
* This is a projection of what the shell has already read, never a second
|
|
5
|
+
* source of truth: the backend is the authority for the list, the focus, and
|
|
6
|
+
* the viewer credential, and a page that is handed a stale generation simply
|
|
7
|
+
* reconnects when the next feed arrives.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
PACKAGE_IFRAME_FOCUS_TOOL_V2,
|
|
11
|
+
type PackageIframeAppletsStateV2,
|
|
12
|
+
type PackageIframeCatalogV1,
|
|
13
|
+
type AppletSummaryV1,
|
|
14
|
+
} from "@frockbot/kernel-contracts";
|
|
15
|
+
import type { FrockBotWebData } from "../shared.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether this Bot's Composition has Applets in it at all.
|
|
19
|
+
*
|
|
20
|
+
* Derived from manifest facts — a Package declaring the Applet focus tool —
|
|
21
|
+
* never from a Package id. A deployment or a User without the Applets Package
|
|
22
|
+
* has no Applet routes, and the shell must not ask for them: an absent
|
|
23
|
+
* capability is silence, not a failed request.
|
|
24
|
+
*/
|
|
25
|
+
export function appletsAvailableV1(
|
|
26
|
+
catalog: PackageIframeCatalogV1 | undefined,
|
|
27
|
+
): boolean {
|
|
28
|
+
return (catalog?.contributions ?? []).some((contribution) =>
|
|
29
|
+
contribution.declaredTools.includes(PACKAGE_IFRAME_FOCUS_TOOL_V2),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function focusedAppletSummaryV1(
|
|
34
|
+
web: Pick<FrockBotWebData, "applets" | "focusedAppletId">,
|
|
35
|
+
): AppletSummaryV1 | null {
|
|
36
|
+
const appletId = web.focusedAppletId;
|
|
37
|
+
if (!appletId) return null;
|
|
38
|
+
return web.applets.find((applet) => applet.appletId === appletId) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function appletsBridgeStateV2(
|
|
42
|
+
web: Pick<
|
|
43
|
+
FrockBotWebData,
|
|
44
|
+
"applets" | "focusedAppletId" | "appletViewer" | "appletBuild"
|
|
45
|
+
>,
|
|
46
|
+
): PackageIframeAppletsStateV2 {
|
|
47
|
+
const viewer = web.appletViewer;
|
|
48
|
+
return {
|
|
49
|
+
focused: focusedAppletSummaryV1(web),
|
|
50
|
+
list: web.applets,
|
|
51
|
+
viewer:
|
|
52
|
+
viewer && viewer.appletId === web.focusedAppletId
|
|
53
|
+
? {
|
|
54
|
+
token: viewer.token,
|
|
55
|
+
socketUrl: viewer.socketUrl,
|
|
56
|
+
uiUrl: viewer.uiUrl,
|
|
57
|
+
generationId: viewer.generationId,
|
|
58
|
+
}
|
|
59
|
+
: null,
|
|
60
|
+
// The source stays out of the feed: the shell draws the code view itself,
|
|
61
|
+
// and a source tree would overflow the bridge's 64 KB message bound.
|
|
62
|
+
...(web.appletBuild === undefined ? {} : { build: web.appletBuild }),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/client/index.test.ts
CHANGED
|
@@ -408,13 +408,19 @@ describe("Bot selection", () => {
|
|
|
408
408
|
packageId: "weather-page",
|
|
409
409
|
displayName: "Sydney Weather",
|
|
410
410
|
provenance: "Bot-authored" as const,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
411
|
+
pages: [
|
|
412
|
+
{
|
|
413
|
+
id: "main",
|
|
414
|
+
artifact: {
|
|
415
|
+
contentHash: "a".repeat(64),
|
|
416
|
+
size: 1,
|
|
417
|
+
mediaType: "text/html" as const,
|
|
418
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
419
|
+
},
|
|
420
|
+
mounts: [{ slot: "frockbot.bot-settings-sections", order: 20 }],
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
entries: [],
|
|
418
424
|
declaredTools: ["weather_lookup"],
|
|
419
425
|
};
|
|
420
426
|
const requests: Array<{
|