@frockbot/plugin-shell 0.1.3 → 0.2.0
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/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +23 -5
- package/src/backend-configuration.test.ts +549 -1468
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-execution.ts +0 -4
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +230 -89
- package/src/backend-isolate.ts +103 -200
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +1198 -1781
- package/src/client/FrockBotApp.vue +44 -73
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +439 -380
- package/src/client/index.ts +163 -257
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/client/styles.css +0 -27
- package/src/composition-views.ts +54 -0
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +10 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { clientSurfaceRegistryKey } from "@frockbot/client-core";
|
|
3
3
|
import {
|
|
4
4
|
announceUiAnchor,
|
|
5
|
-
UiButton,
|
|
6
5
|
UiIcon,
|
|
7
6
|
UiIconButton,
|
|
8
7
|
UiMarkdown,
|
|
@@ -24,9 +23,11 @@ import {
|
|
|
24
23
|
type WebChatMessage,
|
|
25
24
|
type WebTaskChip,
|
|
26
25
|
type WebToolAttachment,
|
|
26
|
+
type WebToolActivity,
|
|
27
27
|
} from "../shared.js";
|
|
28
28
|
import { ComposerDraftStore } from "./composer-draft.js";
|
|
29
29
|
import SendPayloadView from "./SendPayloadView.vue";
|
|
30
|
+
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
30
31
|
import type { ClientSkillCatalogEntryV1 } from "../skill-protocol.js";
|
|
31
32
|
import {
|
|
32
33
|
nextSkillHighlightV1,
|
|
@@ -168,33 +169,6 @@ const botName = computed(
|
|
|
168
169
|
);
|
|
169
170
|
const isRunning = computed(() => Boolean(state.value.activeRunId));
|
|
170
171
|
const isConnecting = computed(() => state.value.connection !== "ready");
|
|
171
|
-
const needsModel = computed(() => state.value.modelSource === "none");
|
|
172
|
-
const hasConnectedModelProvider = computed(() => {
|
|
173
|
-
const user = state.value.userSettings;
|
|
174
|
-
if (!user) return false;
|
|
175
|
-
return user.connections.some((connection) => {
|
|
176
|
-
if (connection.state !== "ready") return false;
|
|
177
|
-
const installed = user.packages.some(
|
|
178
|
-
(pkg) =>
|
|
179
|
-
pkg.packageId === connection.packageId && pkg.state === "installed",
|
|
180
|
-
);
|
|
181
|
-
const pkg = state.value.pluginCatalog.find(
|
|
182
|
-
(candidate) => candidate.packageId === connection.packageId,
|
|
183
|
-
);
|
|
184
|
-
const connectionType = pkg?.connectionTypes.find(
|
|
185
|
-
(candidate) => candidate.id === connection.connectionTypeId,
|
|
186
|
-
);
|
|
187
|
-
return Boolean(
|
|
188
|
-
installed &&
|
|
189
|
-
pkg?.capabilities.some(
|
|
190
|
-
(capability) =>
|
|
191
|
-
capability.kind === "model" &&
|
|
192
|
-
connectionType?.capabilities.includes(capability.id) &&
|
|
193
|
-
capability.connectionTypes.includes(connectionType.id),
|
|
194
|
-
),
|
|
195
|
-
);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
172
|
const canSend = computed(
|
|
199
173
|
() =>
|
|
200
174
|
state.value.connection === "ready" &&
|
|
@@ -217,6 +191,30 @@ function attachmentsOf(message: WebChatMessage): WebToolAttachment[] {
|
|
|
217
191
|
return message.tools.flatMap((tool) => tool.attachments ?? []);
|
|
218
192
|
}
|
|
219
193
|
|
|
194
|
+
function iframeEntriesFor(tool: WebToolActivity) {
|
|
195
|
+
const slot = `frockbot.tool-result:${tool.name}`;
|
|
196
|
+
return (state.value.packageUi?.contributions ?? [])
|
|
197
|
+
.flatMap((contribution) =>
|
|
198
|
+
contribution.mounts
|
|
199
|
+
.filter((mount) => mount.slot === slot)
|
|
200
|
+
.map((mount) => ({ contribution, slot, order: mount.order ?? 0 })),
|
|
201
|
+
)
|
|
202
|
+
.sort(
|
|
203
|
+
(left, right) =>
|
|
204
|
+
left.order - right.order ||
|
|
205
|
+
left.contribution.packageId.localeCompare(right.contribution.packageId),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function toolResultState(tool: WebToolActivity): unknown {
|
|
210
|
+
if (tool.text === undefined) return { status: tool.status };
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(tool.text) as unknown;
|
|
213
|
+
} catch {
|
|
214
|
+
return { content: tool.text, isError: tool.status === "failed" };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
220
218
|
/** The Workspace read route for one encoded `WorkspacePathV1`. */
|
|
221
219
|
function workspaceFileUrl(path: string): string {
|
|
222
220
|
const botId = state.value.activeBotId ?? "";
|
|
@@ -279,6 +277,7 @@ function isVisible(message: WebChatMessage): boolean {
|
|
|
279
277
|
// visible act was dispatching a subagent.
|
|
280
278
|
return (
|
|
281
279
|
message.text.length > 0 ||
|
|
280
|
+
message.tools.some((tool) => iframeEntriesFor(tool).length > 0) ||
|
|
282
281
|
message.sends.length > 0 ||
|
|
283
282
|
(message.tasks?.length ?? 0) > 0 ||
|
|
284
283
|
message.status === "streaming"
|
|
@@ -380,17 +379,6 @@ const applySettingsDeepLink = (): void => {
|
|
|
380
379
|
if (anchor) void nextTick(() => announceUiAnchor(anchor));
|
|
381
380
|
};
|
|
382
381
|
|
|
383
|
-
function openModelSetup(): void {
|
|
384
|
-
const registry = surfaces;
|
|
385
|
-
if (!registry) return;
|
|
386
|
-
if (hasConnectedModelProvider.value && registry.has("user-settings")) {
|
|
387
|
-
registry.open("user-settings");
|
|
388
|
-
void nextTick(() => announceUiAnchor("user-default-model"));
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (registry.has("plugins")) registry.open("plugins");
|
|
392
|
-
}
|
|
393
|
-
|
|
394
382
|
onMounted(() => {
|
|
395
383
|
void web.value.loadPluginCatalog();
|
|
396
384
|
void scrollToLatest("auto");
|
|
@@ -665,15 +653,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
665
653
|
/></span>
|
|
666
654
|
<div class="workspace-title">
|
|
667
655
|
<strong>{{ botName }}</strong>
|
|
668
|
-
<small
|
|
669
|
-
<button
|
|
670
|
-
v-else
|
|
671
|
-
type="button"
|
|
672
|
-
class="model-setup-link"
|
|
673
|
-
@click="openModelSetup"
|
|
674
|
-
>
|
|
675
|
-
Choose a model
|
|
676
|
-
</button>
|
|
656
|
+
<small>{{ state.modelLabel }}</small>
|
|
677
657
|
</div>
|
|
678
658
|
<k-slot name="frockbot.header-actions" />
|
|
679
659
|
</header>
|
|
@@ -690,27 +670,16 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
690
670
|
{{
|
|
691
671
|
state.modelReady
|
|
692
672
|
? `${botName} is ready.`
|
|
693
|
-
:
|
|
694
|
-
? `${botName} needs a model.`
|
|
695
|
-
: `${botName} isn't ready.`
|
|
673
|
+
: `${botName} isn't ready.`
|
|
696
674
|
}}
|
|
697
675
|
</h1>
|
|
698
676
|
<p>
|
|
699
677
|
{{
|
|
700
678
|
state.modelReady
|
|
701
679
|
? "Start with a conversation. Cordis plugins can add the rest."
|
|
702
|
-
:
|
|
703
|
-
? "Pick a default to start chatting."
|
|
704
|
-
: "Check this Bot's model Connection."
|
|
680
|
+
: "Check this Bot's model Connection."
|
|
705
681
|
}}
|
|
706
682
|
</p>
|
|
707
|
-
<UiButton
|
|
708
|
-
v-if="needsModel"
|
|
709
|
-
variant="primary"
|
|
710
|
-
@click="openModelSetup"
|
|
711
|
-
>
|
|
712
|
-
Choose a model
|
|
713
|
-
</UiButton>
|
|
714
683
|
</div>
|
|
715
684
|
<article
|
|
716
685
|
v-for="message in messages"
|
|
@@ -745,8 +714,19 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
745
714
|
<div v-if="message.text" class="message-bubble">
|
|
746
715
|
<UiMarkdown :text="message.text" />
|
|
747
716
|
</div>
|
|
717
|
+
<template v-for="tool in message.tools" :key="tool.id">
|
|
718
|
+
<PackageIframeHost
|
|
719
|
+
v-for="entry in iframeEntriesFor(tool)"
|
|
720
|
+
:key="`${tool.id}:${entry.contribution.packageId}`"
|
|
721
|
+
class="message-package-iframe"
|
|
722
|
+
:contribution="entry.contribution"
|
|
723
|
+
:slot="entry.slot"
|
|
724
|
+
:state-name="`tool:${tool.name}`"
|
|
725
|
+
:state-value="toolResultState(tool)"
|
|
726
|
+
/>
|
|
727
|
+
</template>
|
|
748
728
|
<!--
|
|
749
|
-
A binary a tool filed in
|
|
729
|
+
A binary a tool filed in the Workspace, drawn from the
|
|
750
730
|
Workspace read route. The thread carries the path, never the
|
|
751
731
|
bytes, so a long conversation costs paths and the image is
|
|
752
732
|
fetched only when it is on screen.
|
|
@@ -894,16 +874,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
894
874
|
</span>
|
|
895
875
|
</li>
|
|
896
876
|
</ul>
|
|
897
|
-
<
|
|
898
|
-
v-if="!isRunning && !isConnecting && needsModel"
|
|
899
|
-
type="button"
|
|
900
|
-
class="composer-model-setup"
|
|
901
|
-
variant="primary"
|
|
902
|
-
@click="openModelSetup"
|
|
903
|
-
>
|
|
904
|
-
Choose a model
|
|
905
|
-
</UiButton>
|
|
906
|
-
<div v-else class="composer-body">
|
|
877
|
+
<div class="composer-body">
|
|
907
878
|
<ul v-if="attachedSkills.length > 0" class="skill-chips">
|
|
908
879
|
<li v-for="entry in attachedSkills" :key="entry.ref">
|
|
909
880
|
<button
|
|
@@ -950,7 +921,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
950
921
|
@click="web.stopRun()"
|
|
951
922
|
/>
|
|
952
923
|
<UiIconButton
|
|
953
|
-
v-else
|
|
924
|
+
v-else
|
|
954
925
|
type="submit"
|
|
955
926
|
icon="arrow-up"
|
|
956
927
|
label="Send message"
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
decodePackageIframePageMessageV1,
|
|
4
|
+
packageIframeToolAllowedV1,
|
|
5
|
+
type PackageIframeContributionViewV1,
|
|
6
|
+
type PackageIframeHostMessageV1,
|
|
7
|
+
} from "@frockbot/kernel-contracts";
|
|
8
|
+
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
9
|
+
import { frockBotWebDataKey } from "../shared.js";
|
|
10
|
+
import { postPackageIframeHostMessage } from "./package-iframe-host-message.js";
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
contribution: PackageIframeContributionViewV1;
|
|
14
|
+
slot: string;
|
|
15
|
+
stateName: string;
|
|
16
|
+
stateValue: unknown;
|
|
17
|
+
}>();
|
|
18
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
19
|
+
if (!providedWeb) throw new Error("Package iframe host data was not provided");
|
|
20
|
+
const web = providedWeb;
|
|
21
|
+
const frame = ref<HTMLIFrameElement>();
|
|
22
|
+
const height = ref(240);
|
|
23
|
+
const failure = ref<string>();
|
|
24
|
+
const lastStateWireByName = new Map<string, string>();
|
|
25
|
+
const catalog = computed(() => web.value.packageUi);
|
|
26
|
+
const source = computed(() => {
|
|
27
|
+
const origin = catalog.value?.artifactOrigin;
|
|
28
|
+
return origin
|
|
29
|
+
? `${origin}/packages/${props.contribution.artifact.contentHash}.html`
|
|
30
|
+
: "about:blank";
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const THEME_TOKEN_NAMES = [
|
|
34
|
+
"surface",
|
|
35
|
+
"surface-raised",
|
|
36
|
+
"surface-subtle",
|
|
37
|
+
"text",
|
|
38
|
+
"text-muted",
|
|
39
|
+
"border",
|
|
40
|
+
"accent-surface",
|
|
41
|
+
"accent-text",
|
|
42
|
+
"radius-card",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
function themeTokens(): Record<string, string> {
|
|
46
|
+
const styles = getComputedStyle(document.documentElement);
|
|
47
|
+
return Object.fromEntries(
|
|
48
|
+
THEME_TOKEN_NAMES.map((name) => [
|
|
49
|
+
name,
|
|
50
|
+
styles.getPropertyValue(`--frock-${name}`).trim(),
|
|
51
|
+
]),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function post(message: PackageIframeHostMessageV1): void {
|
|
56
|
+
const target = frame.value?.contentWindow;
|
|
57
|
+
if (!target) return;
|
|
58
|
+
try {
|
|
59
|
+
postPackageIframeHostMessage(target, message, lastStateWireByName);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
failure.value =
|
|
62
|
+
error instanceof Error ? error.message : "Package page state is invalid";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function initialize(): void {
|
|
67
|
+
const botId = web.value.activeBotId;
|
|
68
|
+
if (!botId) return;
|
|
69
|
+
lastStateWireByName.clear();
|
|
70
|
+
post({
|
|
71
|
+
schemaVersion: 1,
|
|
72
|
+
type: "init",
|
|
73
|
+
themeTokens: themeTokens(),
|
|
74
|
+
packageId: props.contribution.packageId,
|
|
75
|
+
botId,
|
|
76
|
+
slot: props.slot,
|
|
77
|
+
});
|
|
78
|
+
post({
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
type: "state",
|
|
81
|
+
name: props.stateName,
|
|
82
|
+
value: props.stateValue,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
watch(
|
|
87
|
+
() => [props.stateName, props.stateValue] as const,
|
|
88
|
+
() =>
|
|
89
|
+
post({
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
type: "state",
|
|
92
|
+
name: props.stateName,
|
|
93
|
+
value: props.stateValue,
|
|
94
|
+
}),
|
|
95
|
+
{ deep: true },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
async function onMessage(event: MessageEvent): Promise<void> {
|
|
99
|
+
if (!frame.value?.contentWindow || event.source !== frame.value.contentWindow)
|
|
100
|
+
return;
|
|
101
|
+
let message;
|
|
102
|
+
try {
|
|
103
|
+
message = decodePackageIframePageMessageV1(event.data);
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (message.type === "resize") {
|
|
108
|
+
height.value = Math.min(1_200, Math.max(96, Math.round(message.height)));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!packageIframeToolAllowedV1(props.contribution, message.name)) {
|
|
112
|
+
failure.value = `This Package did not declare ${message.name}.`;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
failure.value = undefined;
|
|
116
|
+
try {
|
|
117
|
+
const result = await web.value.callPackageUiTool(
|
|
118
|
+
props.contribution,
|
|
119
|
+
message.name,
|
|
120
|
+
message.input,
|
|
121
|
+
);
|
|
122
|
+
post({
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
type: "state",
|
|
125
|
+
name: `tool:${message.name}`,
|
|
126
|
+
value: result,
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
failure.value = error instanceof Error ? error.message : "Tool call failed";
|
|
130
|
+
post({
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
type: "state",
|
|
133
|
+
name: `tool:${message.name}`,
|
|
134
|
+
value: { isError: true, content: failure.value },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onMounted(() => window.addEventListener("message", onMessage));
|
|
140
|
+
onBeforeUnmount(() => window.removeEventListener("message", onMessage));
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<template>
|
|
144
|
+
<section class="package-iframe-frame">
|
|
145
|
+
<header class="package-iframe-attribution">
|
|
146
|
+
<strong>{{ contribution.displayName }}</strong>
|
|
147
|
+
<span>{{ contribution.provenance }} Package</span>
|
|
148
|
+
</header>
|
|
149
|
+
<!-- Load eagerly because lazy iframes defer the init/resize handshake until the browser decides the frame is near the viewport, which headless Chromium may never do. -->
|
|
150
|
+
<iframe
|
|
151
|
+
ref="frame"
|
|
152
|
+
:title="`${contribution.displayName} Package page`"
|
|
153
|
+
:src="source"
|
|
154
|
+
:style="{ height: `${height}px` }"
|
|
155
|
+
sandbox="allow-scripts"
|
|
156
|
+
credentialless
|
|
157
|
+
referrerpolicy="no-referrer"
|
|
158
|
+
@load="initialize"
|
|
159
|
+
/>
|
|
160
|
+
<p v-if="failure" class="package-iframe-failure" role="alert">
|
|
161
|
+
{{ failure }}
|
|
162
|
+
</p>
|
|
163
|
+
</section>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<style scoped>
|
|
167
|
+
.package-iframe-frame {
|
|
168
|
+
min-width: 0;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
border: 1px solid var(--frock-border);
|
|
171
|
+
border-radius: var(--frock-radius-card);
|
|
172
|
+
background: var(--frock-surface);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.package-iframe-attribution {
|
|
176
|
+
position: relative;
|
|
177
|
+
z-index: 1;
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: baseline;
|
|
180
|
+
justify-content: space-between;
|
|
181
|
+
gap: 12px;
|
|
182
|
+
min-height: 36px;
|
|
183
|
+
padding: 8px 12px;
|
|
184
|
+
border-bottom: 1px solid var(--frock-border);
|
|
185
|
+
color: var(--frock-text);
|
|
186
|
+
background: var(--frock-surface-subtle);
|
|
187
|
+
font-size: var(--frock-text-xs);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.package-iframe-attribution span,
|
|
191
|
+
.package-iframe-failure {
|
|
192
|
+
color: var(--frock-text-muted);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
iframe {
|
|
196
|
+
display: block;
|
|
197
|
+
width: 100%;
|
|
198
|
+
max-width: 100%;
|
|
199
|
+
border: 0;
|
|
200
|
+
background: transparent;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.package-iframe-failure {
|
|
204
|
+
margin: 0;
|
|
205
|
+
padding: 8px 12px;
|
|
206
|
+
border-top: 1px solid var(--frock-danger-border);
|
|
207
|
+
color: var(--frock-danger-text);
|
|
208
|
+
font-size: var(--frock-text-xs);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@media (max-width: 640px) {
|
|
212
|
+
.package-iframe-attribution {
|
|
213
|
+
align-items: flex-start;
|
|
214
|
+
flex-direction: column;
|
|
215
|
+
gap: 2px;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import { frockBotWebDataKey } from "../shared.js";
|
|
4
|
+
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
5
|
+
|
|
6
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
7
|
+
if (!providedWeb)
|
|
8
|
+
throw new Error("Package iframe settings data was not provided");
|
|
9
|
+
const web = providedWeb;
|
|
10
|
+
const slot = "frockbot.bot-settings-sections";
|
|
11
|
+
const contributions = computed(() =>
|
|
12
|
+
(web.value.packageUi?.contributions ?? [])
|
|
13
|
+
.flatMap((contribution) =>
|
|
14
|
+
contribution.mounts
|
|
15
|
+
.filter((mount) => mount.slot === slot)
|
|
16
|
+
.map((mount) => ({ contribution, order: mount.order ?? 0 })),
|
|
17
|
+
)
|
|
18
|
+
.sort(
|
|
19
|
+
(left, right) =>
|
|
20
|
+
left.order - right.order ||
|
|
21
|
+
left.contribution.packageId.localeCompare(right.contribution.packageId),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function settingsFor(packageId: string): Record<string, unknown> {
|
|
26
|
+
const installation = web.value.userSettings?.packages.find(
|
|
27
|
+
(candidate) => candidate.packageId === packageId,
|
|
28
|
+
);
|
|
29
|
+
return installation?.values ?? {};
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div v-if="contributions.length > 0" class="package-iframe-settings">
|
|
35
|
+
<PackageIframeHost
|
|
36
|
+
v-for="entry in contributions"
|
|
37
|
+
:key="entry.contribution.packageId"
|
|
38
|
+
:contribution="entry.contribution"
|
|
39
|
+
:slot="slot"
|
|
40
|
+
state-name="settings"
|
|
41
|
+
:state-value="settingsFor(entry.contribution.packageId)"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<style scoped>
|
|
47
|
+
.package-iframe-settings {
|
|
48
|
+
display: grid;
|
|
49
|
+
gap: 16px;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { UiButton, UiMarkdown } from "@frockbot/client-ui";
|
|
21
21
|
import { computed, inject, ref } from "vue";
|
|
22
|
-
import { settingsLinkV1 } from "../settings-links.js";
|
|
23
22
|
import { frockBotWebDataKey, type WebSendPayload } from "../shared.js";
|
|
24
23
|
|
|
25
24
|
const props = defineProps<{ send: WebSendPayload }>();
|
|
@@ -39,17 +38,6 @@ const widget = computed(() =>
|
|
|
39
38
|
const attachment = computed(() =>
|
|
40
39
|
payload.value?.type === "attachment" ? payload.value : undefined,
|
|
41
40
|
);
|
|
42
|
-
/**
|
|
43
|
-
* The connect card. It carries no URL and never will: the Bot recorded a
|
|
44
|
-
* pending decision, and only the User — in Settings, where the host authors
|
|
45
|
-
* the link at the moment they press it — can complete one. So this draws the
|
|
46
|
-
* request and points at the place the decision is made, and is deliberately
|
|
47
|
-
* not a button that authorizes anything.
|
|
48
|
-
*/
|
|
49
|
-
const connectCard = computed(() =>
|
|
50
|
-
payload.value?.type === "connect-card" ? payload.value : undefined,
|
|
51
|
-
);
|
|
52
|
-
const connectionsLink = settingsLinkV1({ anchor: "user-packages" });
|
|
53
41
|
const approval = computed(() =>
|
|
54
42
|
payload.value?.type === "approval" ? payload.value : undefined,
|
|
55
43
|
);
|
|
@@ -115,21 +103,6 @@ const unsupported = computed(
|
|
|
115
103
|
</p>
|
|
116
104
|
</section>
|
|
117
105
|
|
|
118
|
-
<section
|
|
119
|
-
v-else-if="connectCard"
|
|
120
|
-
class="send-connect-card"
|
|
121
|
-
aria-label="Connection request"
|
|
122
|
-
>
|
|
123
|
-
<p class="send-connect-title">{{ connectCard.title }}</p>
|
|
124
|
-
<p v-if="connectCard.body" class="send-connect-body">
|
|
125
|
-
{{ connectCard.body }}
|
|
126
|
-
</p>
|
|
127
|
-
<p class="send-connect-help">
|
|
128
|
-
Open Settings → Plugins to authorize it. Only you can complete this.
|
|
129
|
-
<a :href="connectionsLink">{{ connectionsLink }}</a>
|
|
130
|
-
</p>
|
|
131
|
-
</section>
|
|
132
|
-
|
|
133
106
|
<section
|
|
134
107
|
v-else-if="approval"
|
|
135
108
|
class="send-approval"
|
|
@@ -221,39 +194,6 @@ const unsupported = computed(
|
|
|
221
194
|
list-style: none;
|
|
222
195
|
}
|
|
223
196
|
|
|
224
|
-
.send-connect-card {
|
|
225
|
-
display: flex;
|
|
226
|
-
flex-direction: column;
|
|
227
|
-
gap: 0.5rem;
|
|
228
|
-
border: 1px solid var(--frock-border);
|
|
229
|
-
border-radius: var(--frock-radius-card);
|
|
230
|
-
background: var(--frock-surface-raised);
|
|
231
|
-
padding: 0.75rem 1rem;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.send-connect-title {
|
|
235
|
-
margin: 0;
|
|
236
|
-
color: var(--frock-text);
|
|
237
|
-
font-weight: 600;
|
|
238
|
-
font-size: var(--frock-text-base);
|
|
239
|
-
line-height: var(--frock-leading-snug);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.send-connect-body {
|
|
243
|
-
margin: 0;
|
|
244
|
-
color: var(--frock-text);
|
|
245
|
-
font-size: var(--frock-text-sm);
|
|
246
|
-
line-height: var(--frock-leading-normal);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.send-connect-help {
|
|
250
|
-
margin: 0;
|
|
251
|
-
color: var(--frock-text-muted);
|
|
252
|
-
font-size: var(--frock-text-sm);
|
|
253
|
-
line-height: var(--frock-leading-normal);
|
|
254
|
-
overflow-wrap: anywhere;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
197
|
.send-widget-options li {
|
|
258
198
|
border: 1px solid var(--frock-border);
|
|
259
199
|
border-radius: var(--frock-radius-control);
|