@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.
@@ -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();
@@ -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: 6px;
738
- padding: 0 10px;
776
+ gap: 8px;
777
+ padding: 6px 10px;
739
778
  border: 1px solid var(--frock-border-strong);
740
- border-radius: 999px;
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);
@@ -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[];