@frockbot/plugin-shell 0.3.22 → 0.3.23
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 -32
- package/src/backend-recovery-integration.test.ts +134 -0
- package/src/backend.ts +93 -42
- package/src/client/AppletCanvas.vue +128 -30
- package/src/client/FrockBotApp.vue +111 -2
- package/src/client/applet-building-view.test.ts +69 -0
- package/src/client/applet-progress.test.ts +300 -0
- package/src/client/applet-progress.ts +339 -0
- package/src/client/deployment.test.ts +152 -0
- package/src/client/deployment.ts +138 -0
- package/src/client/index.test.ts +71 -0
- package/src/client/index.ts +58 -0
- package/src/client/styles.css +91 -4
- package/src/run-protocol.ts +10 -0
- package/src/shared.ts +14 -0
package/src/client/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { clientSurfaceRegistryKey } from "@frockbot/client-core";
|
|
17
17
|
import { COMPACTED_ANNOUNCEMENT_TEXT_V1 } from "../compaction.js";
|
|
18
18
|
import { voiceCaptureSupportedV1 } from "./voice-microphone.js";
|
|
19
|
+
import { deploymentStaleV1 } from "./deployment.js";
|
|
19
20
|
import { readViewerFocusV1, shouldNotifyForBotV1 } from "../focus.js";
|
|
20
21
|
// Connection mutations use the provider-neutral hosted command contract.
|
|
21
22
|
import type {
|
|
@@ -1433,6 +1434,19 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1433
1434
|
voiceAvailable:
|
|
1434
1435
|
typeof ctx.transport.openVoiceDictation === "function" &&
|
|
1435
1436
|
voiceCaptureSupportedV1(),
|
|
1437
|
+
deploymentStale: false,
|
|
1438
|
+
reloadHolds: 0,
|
|
1439
|
+
holdReload: () => {
|
|
1440
|
+
web.value.reloadHolds += 1;
|
|
1441
|
+
let released = false;
|
|
1442
|
+
return () => {
|
|
1443
|
+
// Idempotent: a Package that lets go twice must not leave the count
|
|
1444
|
+
// below zero, which would read as "another holder released early".
|
|
1445
|
+
if (released) return;
|
|
1446
|
+
released = true;
|
|
1447
|
+
web.value.reloadHolds -= 1;
|
|
1448
|
+
};
|
|
1449
|
+
},
|
|
1436
1450
|
activeBotId: undefined,
|
|
1437
1451
|
composerContext: undefined,
|
|
1438
1452
|
transcripts: {
|
|
@@ -3161,6 +3175,43 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
3161
3175
|
{ immediate: true },
|
|
3162
3176
|
);
|
|
3163
3177
|
|
|
3178
|
+
/*
|
|
3179
|
+
* Following a release in a page that is already open.
|
|
3180
|
+
*
|
|
3181
|
+
* Every answer names the application that produced it and the request layer
|
|
3182
|
+
* reports it here, so a page whose own application has stopped answering is
|
|
3183
|
+
* running code the backend has moved past. Nothing polls for this: the
|
|
3184
|
+
* reads the app already makes carry the answer.
|
|
3185
|
+
*/
|
|
3186
|
+
const stopDeploymentWatch = ctx.transport.observeDeployment?.((answered) => {
|
|
3187
|
+
if (deploymentStaleV1(ctx.transport.servedDeployment, answered)) {
|
|
3188
|
+
web.value.deploymentStale = true;
|
|
3189
|
+
}
|
|
3190
|
+
});
|
|
3191
|
+
/*
|
|
3192
|
+
* A tab that has been in the background for a day should not have to wait
|
|
3193
|
+
* for the next scheduled read to find out. Coming back asks for the
|
|
3194
|
+
* manifest the client already reads at start-up rather than adding a poll
|
|
3195
|
+
* of its own, and only while the answer is still unknown.
|
|
3196
|
+
*/
|
|
3197
|
+
const readManifest = ctx.transport.readApplicationManifest?.bind(
|
|
3198
|
+
ctx.transport,
|
|
3199
|
+
);
|
|
3200
|
+
const followDeploymentOnReturn = () => {
|
|
3201
|
+
if (document.visibilityState !== "visible") return;
|
|
3202
|
+
if (web.value.deploymentStale || !readManifest) return;
|
|
3203
|
+
void readManifest().catch(() => {
|
|
3204
|
+
// A read that failed says nothing about the deployment. The next one,
|
|
3205
|
+
// or the next answer of any kind, will.
|
|
3206
|
+
});
|
|
3207
|
+
};
|
|
3208
|
+
// The Plugin is also installed without a document — the unit tests drive it
|
|
3209
|
+
// headless — and there a tab can neither be hidden nor come back.
|
|
3210
|
+
const inDocument = typeof document !== "undefined";
|
|
3211
|
+
if (inDocument) {
|
|
3212
|
+
document.addEventListener("visibilitychange", followDeploymentOnReturn);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3164
3215
|
return [
|
|
3165
3216
|
ctx.provide(clientSurfaceRegistryKey, surfaces),
|
|
3166
3217
|
// The shared client projection is updated by the contracts lane. This cast
|
|
@@ -3177,6 +3228,13 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
3177
3228
|
component: PackageIframeSettings,
|
|
3178
3229
|
}),
|
|
3179
3230
|
() => {
|
|
3231
|
+
stopDeploymentWatch?.();
|
|
3232
|
+
if (inDocument) {
|
|
3233
|
+
document.removeEventListener(
|
|
3234
|
+
"visibilitychange",
|
|
3235
|
+
followDeploymentOnReturn,
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3180
3238
|
stopEntrySync();
|
|
3181
3239
|
stopRunFollow();
|
|
3182
3240
|
stopRunChannelWatch();
|
package/src/client/styles.css
CHANGED
|
@@ -644,6 +644,45 @@
|
|
|
644
644
|
cursor: pointer;
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
+
/*
|
|
648
|
+
* The release bar. It sits where the error banner sits and only one shows at
|
|
649
|
+
* a time, so it borrows that geometry and none of its colour: nothing here is
|
|
650
|
+
* wrong, there is just newer code waiting.
|
|
651
|
+
*/
|
|
652
|
+
.update-banner {
|
|
653
|
+
position: absolute;
|
|
654
|
+
right: 20px;
|
|
655
|
+
bottom: 84px;
|
|
656
|
+
left: 20px;
|
|
657
|
+
z-index: 5;
|
|
658
|
+
display: flex;
|
|
659
|
+
min-height: 38px;
|
|
660
|
+
align-items: center;
|
|
661
|
+
justify-content: space-between;
|
|
662
|
+
gap: 12px;
|
|
663
|
+
padding: 7px 12px;
|
|
664
|
+
border: 1px solid var(--frock-border);
|
|
665
|
+
border-radius: 12px;
|
|
666
|
+
color: var(--frock-text);
|
|
667
|
+
background: var(--frock-surface-raised);
|
|
668
|
+
box-shadow: var(--frock-shadow-card);
|
|
669
|
+
font-size: var(--frock-text-sm);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.update-banner button {
|
|
673
|
+
flex: 0 0 auto;
|
|
674
|
+
padding: 5px 10px;
|
|
675
|
+
border-radius: 999px;
|
|
676
|
+
color: var(--frock-on-accent);
|
|
677
|
+
background: var(--frock-action-primary);
|
|
678
|
+
font-weight: 600;
|
|
679
|
+
cursor: pointer;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.update-banner button:hover {
|
|
683
|
+
background: var(--frock-action-primary-hover);
|
|
684
|
+
}
|
|
685
|
+
|
|
647
686
|
.banner-enter-active,
|
|
648
687
|
.banner-leave-active {
|
|
649
688
|
transition:
|
|
@@ -732,25 +771,73 @@
|
|
|
732
771
|
left: 0;
|
|
733
772
|
display: inline-flex;
|
|
734
773
|
max-width: 100%;
|
|
735
|
-
height: var(--frock-control-md);
|
|
774
|
+
min-height: var(--frock-control-md);
|
|
736
775
|
align-items: center;
|
|
737
|
-
gap:
|
|
738
|
-
padding:
|
|
776
|
+
gap: 8px;
|
|
777
|
+
padding: 6px 10px;
|
|
739
778
|
border: 1px solid var(--frock-border-strong);
|
|
740
|
-
border-radius:
|
|
779
|
+
border-radius: var(--frock-radius-card);
|
|
741
780
|
color: var(--frock-text);
|
|
742
781
|
background: var(--frock-surface-raised);
|
|
743
782
|
box-shadow: var(--frock-shadow-control);
|
|
744
783
|
font-size: var(--frock-text-sm);
|
|
784
|
+
text-align: left;
|
|
745
785
|
cursor: pointer;
|
|
746
786
|
}
|
|
747
787
|
|
|
788
|
+
.applet-chip-text {
|
|
789
|
+
display: flex;
|
|
790
|
+
min-width: 0;
|
|
791
|
+
flex-direction: column;
|
|
792
|
+
}
|
|
793
|
+
|
|
748
794
|
.applet-chip-name {
|
|
749
795
|
overflow: hidden;
|
|
750
796
|
white-space: nowrap;
|
|
751
797
|
text-overflow: ellipsis;
|
|
752
798
|
}
|
|
753
799
|
|
|
800
|
+
/*
|
|
801
|
+
* What the Bot is doing to it, so the phone says the same thing the canvas
|
|
802
|
+
* beside a wide conversation says without opening anything.
|
|
803
|
+
*/
|
|
804
|
+
.applet-chip-status {
|
|
805
|
+
display: flex;
|
|
806
|
+
overflow: hidden;
|
|
807
|
+
align-items: center;
|
|
808
|
+
gap: 6px;
|
|
809
|
+
color: var(--frock-text-muted);
|
|
810
|
+
font-size: var(--frock-text-xs);
|
|
811
|
+
white-space: nowrap;
|
|
812
|
+
text-overflow: ellipsis;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.applet-chip-dot {
|
|
816
|
+
width: 6px;
|
|
817
|
+
height: 6px;
|
|
818
|
+
flex: 0 0 auto;
|
|
819
|
+
border-radius: 999px;
|
|
820
|
+
background: var(--frock-action-primary);
|
|
821
|
+
animation: applet-chip-pulse 1.4s ease-in-out infinite;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
@keyframes applet-chip-pulse {
|
|
825
|
+
0%,
|
|
826
|
+
100% {
|
|
827
|
+
opacity: 0.35;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
50% {
|
|
831
|
+
opacity: 1;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
@media (prefers-reduced-motion: reduce) {
|
|
836
|
+
.applet-chip-dot {
|
|
837
|
+
animation: none;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
754
841
|
.applet-chip-action {
|
|
755
842
|
flex: 0 0 auto;
|
|
756
843
|
color: var(--frock-action-primary);
|
package/src/run-protocol.ts
CHANGED
|
@@ -45,6 +45,16 @@ const MAX_SESSION_ID_LENGTH = 320;
|
|
|
45
45
|
const MAX_TASK_DESCRIPTION_BYTES = 800;
|
|
46
46
|
const MAX_TASK_MODEL_BYTES = 512;
|
|
47
47
|
export const CLIENT_RUN_PAGE_LIMIT = 32;
|
|
48
|
+
/**
|
|
49
|
+
* How many run-index candidates one transcript page may walk past.
|
|
50
|
+
*
|
|
51
|
+
* The index is global and the transcript is one conversation, so filling a
|
|
52
|
+
* page can mean stepping over Turns of other conversations and over
|
|
53
|
+
* automation firings. Scanning is cheap — a run record, not its journal — but
|
|
54
|
+
* it is not free, so it stops at a budget and hands back a cursor rather than
|
|
55
|
+
* walking a Bot's whole history inside one request.
|
|
56
|
+
*/
|
|
57
|
+
export const CLIENT_RUN_SCAN_LIMIT = CLIENT_RUN_PAGE_LIMIT * 8;
|
|
48
58
|
export const CLIENT_RUN_LIST_MAX_BYTES = 512_000;
|
|
49
59
|
|
|
50
60
|
export type ClientRunStatusV1 =
|
package/src/shared.ts
CHANGED
|
@@ -269,6 +269,20 @@ export interface FrockBotWebData {
|
|
|
269
269
|
* changes shape and nothing about it moves.
|
|
270
270
|
*/
|
|
271
271
|
voiceAvailable: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* The application answering is no longer the one this page was served, so
|
|
274
|
+
* the code running here is behind a release. FrockBot ships several times a
|
|
275
|
+
* day and a tab stays open for days, so this is ordinary rather than rare.
|
|
276
|
+
*/
|
|
277
|
+
deploymentStale: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Live work a reload would throw away, held by whichever Package holds the
|
|
280
|
+
* work. Counted rather than flagged so two holders at once do not release
|
|
281
|
+
* each other. The shell reloads on its own only at zero.
|
|
282
|
+
*/
|
|
283
|
+
reloadHolds: number;
|
|
284
|
+
/** Keeps the shell from reloading. Call the result to let go. */
|
|
285
|
+
holdReload(): () => void;
|
|
272
286
|
activeBotId?: string;
|
|
273
287
|
composerContext?: unknown;
|
|
274
288
|
messages: WebChatMessage[];
|