@frockbot/plugin-shell 0.3.22 → 0.3.24
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-composition.test.ts +221 -0
- package/src/backend-composition.ts +90 -0
- package/src/backend-recovery-integration.test.ts +134 -0
- package/src/backend.ts +133 -43
- 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/backend.ts
CHANGED
|
@@ -126,12 +126,14 @@ import {
|
|
|
126
126
|
planBotRunRecovery,
|
|
127
127
|
planInterruptedRunRecoveryV1,
|
|
128
128
|
} from "./backend-recovery.js";
|
|
129
|
+
import { COMPOSITION_CURRENT_KEY } from "@frockbot/kernel-do";
|
|
129
130
|
import {
|
|
130
131
|
bootstrapCompositionGeneration,
|
|
131
132
|
createShellCompositionHost,
|
|
132
133
|
type ShellAppletMountOptions,
|
|
133
134
|
type ShellIsolateMountOptions,
|
|
134
135
|
type ShellMountedComposition,
|
|
136
|
+
resolveDeploymentCompositionV1,
|
|
135
137
|
} from "./backend-composition.js";
|
|
136
138
|
import {
|
|
137
139
|
createAppletCapabilityHostV1,
|
|
@@ -404,6 +406,7 @@ import {
|
|
|
404
406
|
import {
|
|
405
407
|
CLIENT_RUN_LIST_MAX_BYTES,
|
|
406
408
|
CLIENT_RUN_PAGE_LIMIT,
|
|
409
|
+
CLIENT_RUN_SCAN_LIMIT,
|
|
407
410
|
clientRunListWireBytes,
|
|
408
411
|
createClientRunListV1,
|
|
409
412
|
createClientRunStopReceiptV1,
|
|
@@ -1229,7 +1232,8 @@ export class ShellBotBackendContribution {
|
|
|
1229
1232
|
// from the previous Turn has already handed the log back (ADR 0030).
|
|
1230
1233
|
await yieldCompactionWorkV1(command.sessionId);
|
|
1231
1234
|
// Before admission, so the pin this Turn takes already carries whatever the
|
|
1232
|
-
// User's Applet directory says now.
|
|
1235
|
+
// deployment ships and whatever the User's Applet directory says now.
|
|
1236
|
+
await this.followDeploymentComposition();
|
|
1233
1237
|
await this.resolveAppletComposition(
|
|
1234
1238
|
{ userId: command.userId, botId: command.botId },
|
|
1235
1239
|
command,
|
|
@@ -1244,6 +1248,7 @@ export class ShellBotBackendContribution {
|
|
|
1244
1248
|
await this.validateIdentity(identity);
|
|
1245
1249
|
const catalog = await this.listPackageUi(identity);
|
|
1246
1250
|
const contribution = requirePackageUiToolDeclarationV1(catalog, command);
|
|
1251
|
+
await this.followDeploymentComposition();
|
|
1247
1252
|
return projectClientTurnV1(
|
|
1248
1253
|
await this.authority.run({
|
|
1249
1254
|
...identity,
|
|
@@ -2722,6 +2727,39 @@ export class ShellBotBackendContribution {
|
|
|
2722
2727
|
return focused;
|
|
2723
2728
|
}
|
|
2724
2729
|
|
|
2730
|
+
/**
|
|
2731
|
+
* Bring this Bot's built-in members up to the deployment before a Turn is
|
|
2732
|
+
* admitted, so a release that changed a first-party manifest or artifact is
|
|
2733
|
+
* a new generation rather than a Bot that cannot mount (2026-09-05, when
|
|
2734
|
+
* every Bot failed every Turn after the Applets list page changed). Outside
|
|
2735
|
+
* the admission transaction like the Applet resolve below; a failure here
|
|
2736
|
+
* leaves the Bot on the generation it has and is visible where that
|
|
2737
|
+
* generation fails to mount.
|
|
2738
|
+
*/
|
|
2739
|
+
private async followDeploymentComposition(): Promise<void> {
|
|
2740
|
+
try {
|
|
2741
|
+
// A Bot with nothing pinned yet bootstraps from this deployment when
|
|
2742
|
+
// its first Turn is admitted; reading the current generation here would
|
|
2743
|
+
// materialize that bootstrap early, and a refused command must leave
|
|
2744
|
+
// storage exactly as it found it.
|
|
2745
|
+
if (
|
|
2746
|
+
(await this.ctx.storage.get<unknown>(COMPOSITION_CURRENT_KEY)) ===
|
|
2747
|
+
undefined
|
|
2748
|
+
)
|
|
2749
|
+
return;
|
|
2750
|
+
await resolveDeploymentCompositionV1({
|
|
2751
|
+
plan: await this.compileApplication(),
|
|
2752
|
+
composition: {
|
|
2753
|
+
current: () => this.authority.composition.current(),
|
|
2754
|
+
propose: (generation, options) =>
|
|
2755
|
+
this.authority.composition.propose(generation, options),
|
|
2756
|
+
},
|
|
2757
|
+
});
|
|
2758
|
+
} catch {
|
|
2759
|
+
// The mount records why; never a wedged Turn on its own.
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2725
2763
|
/**
|
|
2726
2764
|
* Resolve the User's Applet directory into this Bot's next Composition
|
|
2727
2765
|
* generation, before a Turn is admitted.
|
|
@@ -3261,6 +3299,7 @@ export class ShellBotBackendContribution {
|
|
|
3261
3299
|
fire: RoutineFireV1,
|
|
3262
3300
|
): Promise<RoutineFireOutcomeV1> {
|
|
3263
3301
|
try {
|
|
3302
|
+
await this.followDeploymentComposition();
|
|
3264
3303
|
await this.authority.run(
|
|
3265
3304
|
routineTurnCommandV1(identity, fire, new Date().toISOString()),
|
|
3266
3305
|
);
|
|
@@ -4196,6 +4235,7 @@ export class ShellBotBackendContribution {
|
|
|
4196
4235
|
await this.ctx.storage.put(key, { ...context, status: "running" });
|
|
4197
4236
|
let outcome: TaskOutcomeV1;
|
|
4198
4237
|
try {
|
|
4238
|
+
await this.followDeploymentComposition();
|
|
4199
4239
|
await this.authority.run({
|
|
4200
4240
|
...identity,
|
|
4201
4241
|
runId: context.taskId,
|
|
@@ -5833,7 +5873,7 @@ export class ShellBotBackendContribution {
|
|
|
5833
5873
|
|
|
5834
5874
|
const selected = new Map<string, { cursor?: string; run: ClientRunV1 }>();
|
|
5835
5875
|
if (activeRunId) {
|
|
5836
|
-
const active = await this.authority.
|
|
5876
|
+
const active = await this.authority.readStoredRunForDisplay(activeRunId);
|
|
5837
5877
|
// An automation firing occupies the object like any other run, and is
|
|
5838
5878
|
// still not part of the conversation: the visible transcript never
|
|
5839
5879
|
// shows one, running or settled.
|
|
@@ -5842,60 +5882,110 @@ export class ShellBotBackendContribution {
|
|
|
5842
5882
|
run: projectClientRunOrDegradedV1(active),
|
|
5843
5883
|
});
|
|
5844
5884
|
}
|
|
5845
|
-
|
|
5885
|
+
// The run index is global and the transcript is one conversation, so a
|
|
5886
|
+
// page of candidates is not a page of answers: 33 Turns of another
|
|
5887
|
+
// conversation, or of automation, used to come back as an empty,
|
|
5888
|
+
// *untruncated* page that told the client there was nothing older. The
|
|
5889
|
+
// scan cursor now advances over every candidate this call consumed,
|
|
5890
|
+
// whether or not it was kept, so a filtered-out page still says where to
|
|
5891
|
+
// resume; and the scan keeps reading batches, up to a budget, so the
|
|
5892
|
+
// common case answers in one request rather than making the client walk
|
|
5893
|
+
// the history a page at a time. Filtering reads run records only —
|
|
5894
|
+
// hydrating a Turn's journal is what selection costs, not what the scan
|
|
5895
|
+
// costs.
|
|
5846
5896
|
let stoppedEarly = false;
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
const
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5897
|
+
let exhausted = false;
|
|
5898
|
+
let scanCursor: string | undefined;
|
|
5899
|
+
let scanned = 0;
|
|
5900
|
+
let batch = candidates;
|
|
5901
|
+
for (;;) {
|
|
5902
|
+
const available = batch.slice(0, CLIENT_RUN_PAGE_LIMIT);
|
|
5903
|
+
const hasMore = batch.length > CLIENT_RUN_PAGE_LIMIT;
|
|
5904
|
+
for (const candidate of available) {
|
|
5905
|
+
if (selected.has(candidate.runId)) {
|
|
5906
|
+
const current = selected.get(candidate.runId)!;
|
|
5907
|
+
selected.set(candidate.runId, {
|
|
5908
|
+
...current,
|
|
5909
|
+
cursor: candidate.cursor,
|
|
5910
|
+
});
|
|
5911
|
+
scanCursor = candidate.cursor;
|
|
5912
|
+
continue;
|
|
5913
|
+
}
|
|
5914
|
+
const header = await this.authority.readRunHeader(candidate.runId);
|
|
5915
|
+
if (!header || !isVisibleRunV1(header) || !inConversation(header)) {
|
|
5916
|
+
scanCursor = candidate.cursor;
|
|
5917
|
+
continue;
|
|
5918
|
+
}
|
|
5919
|
+
const stored = await this.authority.readStoredRunForDisplay(
|
|
5920
|
+
candidate.runId,
|
|
5867
5921
|
);
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5922
|
+
if (!stored) {
|
|
5923
|
+
scanCursor = candidate.cursor;
|
|
5924
|
+
continue;
|
|
5925
|
+
}
|
|
5926
|
+
const projected = projectClientRunOrDegradedV1(stored);
|
|
5927
|
+
const tentative = [
|
|
5928
|
+
...selected.values(),
|
|
5929
|
+
{ cursor: candidate.cursor, run: projected },
|
|
5930
|
+
];
|
|
5931
|
+
const ordered = tentative
|
|
5932
|
+
.map((entry) => entry.run)
|
|
5933
|
+
.sort(
|
|
5934
|
+
(left, right) =>
|
|
5935
|
+
left.admittedAt.localeCompare(right.admittedAt) ||
|
|
5936
|
+
left.runId.localeCompare(right.runId),
|
|
5937
|
+
);
|
|
5938
|
+
const tentativePage = createClientRunListV1(ordered, {
|
|
5939
|
+
truncated: true,
|
|
5940
|
+
nextCursor: candidate.cursor,
|
|
5941
|
+
});
|
|
5942
|
+
const isNewestTerminal =
|
|
5943
|
+
![...selected.values()].some(
|
|
5944
|
+
(entry) =>
|
|
5945
|
+
entry.run.status === "completed" || entry.run.status === "failed",
|
|
5946
|
+
) &&
|
|
5947
|
+
(projected.status === "completed" || projected.status === "failed");
|
|
5948
|
+
if (
|
|
5949
|
+
selected.size >= CLIENT_RUN_PAGE_LIMIT ||
|
|
5950
|
+
(!isNewestTerminal &&
|
|
5951
|
+
clientRunListWireBytes(tentativePage) > CLIENT_RUN_LIST_MAX_BYTES)
|
|
5952
|
+
) {
|
|
5953
|
+
stoppedEarly = true;
|
|
5954
|
+
break;
|
|
5955
|
+
}
|
|
5956
|
+
selected.set(stored.runId, {
|
|
5957
|
+
cursor: candidate.cursor,
|
|
5958
|
+
run: projected,
|
|
5959
|
+
});
|
|
5960
|
+
scanCursor = candidate.cursor;
|
|
5961
|
+
}
|
|
5962
|
+
scanned += available.length;
|
|
5963
|
+
if (stoppedEarly) break;
|
|
5964
|
+
if (!hasMore) {
|
|
5965
|
+
exhausted = true;
|
|
5966
|
+
break;
|
|
5967
|
+
}
|
|
5968
|
+
if (selected.size >= CLIENT_RUN_PAGE_LIMIT) break;
|
|
5969
|
+
if (scanned >= CLIENT_RUN_SCAN_LIMIT || scanCursor === undefined) break;
|
|
5970
|
+
batch = await this.authority.listRunIndex({
|
|
5971
|
+
limit: CLIENT_RUN_PAGE_LIMIT + 1,
|
|
5972
|
+
before: scanCursor,
|
|
5871
5973
|
});
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
(entry) =>
|
|
5875
|
-
entry.run.status === "completed" || entry.run.status === "failed",
|
|
5876
|
-
) &&
|
|
5877
|
-
(projected.status === "completed" || projected.status === "failed");
|
|
5878
|
-
if (
|
|
5879
|
-
selected.size >= CLIENT_RUN_PAGE_LIMIT ||
|
|
5880
|
-
(!isNewestTerminal &&
|
|
5881
|
-
clientRunListWireBytes(tentativePage) > CLIENT_RUN_LIST_MAX_BYTES)
|
|
5882
|
-
) {
|
|
5883
|
-
stoppedEarly = true;
|
|
5974
|
+
if (batch.length === 0) {
|
|
5975
|
+
exhausted = true;
|
|
5884
5976
|
break;
|
|
5885
5977
|
}
|
|
5886
|
-
selected.set(stored.runId, { cursor: candidate.cursor, run: projected });
|
|
5887
5978
|
}
|
|
5888
5979
|
const orderedEntries = [...selected.values()].sort(
|
|
5889
5980
|
(left, right) =>
|
|
5890
5981
|
left.run.admittedAt.localeCompare(right.run.admittedAt) ||
|
|
5891
5982
|
left.run.runId.localeCompare(right.run.runId),
|
|
5892
5983
|
);
|
|
5893
|
-
const
|
|
5894
|
-
const truncated = stoppedEarly || candidates.length > CLIENT_RUN_PAGE_LIMIT;
|
|
5984
|
+
const truncated = !exhausted;
|
|
5895
5985
|
const page = createClientRunListV1(
|
|
5896
5986
|
orderedEntries.map((entry) => entry.run),
|
|
5897
|
-
truncated &&
|
|
5898
|
-
? { truncated: true, nextCursor:
|
|
5987
|
+
truncated && scanCursor
|
|
5988
|
+
? { truncated: true, nextCursor: scanCursor }
|
|
5899
5989
|
: { truncated: false },
|
|
5900
5990
|
// Announcements belong to the Session, not to a page of Turns, so only
|
|
5901
5991
|
// the newest page carries them.
|
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
appletSourceFingerprintV1,
|
|
23
23
|
mostRecentlyChangedFileV1,
|
|
24
24
|
} from "./applets-client.js";
|
|
25
|
+
import {
|
|
26
|
+
appletIsBeingBuiltV1,
|
|
27
|
+
appletProgressToolsV1,
|
|
28
|
+
appletProgressV1,
|
|
29
|
+
} from "./applet-progress.js";
|
|
25
30
|
import { packageIframePagesForSlotV1 } from "./package-iframe-entries.js";
|
|
26
31
|
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
27
32
|
|
|
@@ -46,6 +51,26 @@ const isRunning = computed(() => Boolean(web.value.activeRunId));
|
|
|
46
51
|
const canvasState = computed(() => web.value.appletCanvas);
|
|
47
52
|
const failure = computed(() => web.value.appletCanvasError);
|
|
48
53
|
|
|
54
|
+
/*
|
|
55
|
+
* What the Bot is doing to this Applet, in one line.
|
|
56
|
+
*
|
|
57
|
+
* A publish is minutes away from a create, and for all of it there is nothing
|
|
58
|
+
* to run. The panel used to answer that with one fixed line about the Applet
|
|
59
|
+
* not being live and a file list, which never changed and never said whether
|
|
60
|
+
* anything was happening. This is the sentence in between, projected from what
|
|
61
|
+
* the thread already shows: no new read, no new durable state.
|
|
62
|
+
*/
|
|
63
|
+
const progress = computed(() =>
|
|
64
|
+
appletProgressV1({
|
|
65
|
+
applet: applet.value ?? null,
|
|
66
|
+
source: source.value,
|
|
67
|
+
build: build.value,
|
|
68
|
+
tools: appletProgressToolsV1(web.value.messages),
|
|
69
|
+
running: isRunning.value,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
const beingBuilt = computed(() => appletIsBeingBuiltV1(progress.value));
|
|
73
|
+
|
|
49
74
|
/** The Applets Package's right-panel page, when its Composition carries one. */
|
|
50
75
|
const panelPage = computed(
|
|
51
76
|
() => packageIframePagesForSlotV1(web.value.packageUi, RIGHT_PANEL_SLOT)[0],
|
|
@@ -212,8 +237,11 @@ async function clearFocus(): Promise<void> {
|
|
|
212
237
|
/></span>
|
|
213
238
|
<div class="applet-canvas-title">
|
|
214
239
|
<strong>{{ applet?.displayName ?? "Applet" }}</strong>
|
|
240
|
+
<!--
|
|
241
|
+
While it is being built the line under the name is the pinned
|
|
242
|
+
building view below, not a copy of it here.
|
|
243
|
+
-->
|
|
215
244
|
<small v-if="viewer">{{ generationLabel(viewer.generationId) }}</small>
|
|
216
|
-
<small v-else>Not published yet</small>
|
|
217
245
|
</div>
|
|
218
246
|
<div
|
|
219
247
|
v-if="canShowApp"
|
|
@@ -255,6 +283,39 @@ async function clearFocus(): Promise<void> {
|
|
|
255
283
|
/>
|
|
256
284
|
</header>
|
|
257
285
|
|
|
286
|
+
<!--
|
|
287
|
+
Where the work has got to, pinned under the header while the Applet is
|
|
288
|
+
still being built: one line a person can read, the reason it stopped when
|
|
289
|
+
it stopped, and the tail of whatever the check or the build printed —
|
|
290
|
+
which is the only thing that helps when the Bot is going round in circles
|
|
291
|
+
on a type error. It sits outside the scrolling column deliberately: a
|
|
292
|
+
person reading the source still wants to know what is happening to it.
|
|
293
|
+
-->
|
|
294
|
+
<section
|
|
295
|
+
v-if="applet && !loading && progress && beingBuilt"
|
|
296
|
+
class="applet-canvas-progress"
|
|
297
|
+
:class="{ 'applet-canvas-progress-wrong': Boolean(progress.failure) }"
|
|
298
|
+
data-testid="applet-canvas-progress"
|
|
299
|
+
role="status"
|
|
300
|
+
>
|
|
301
|
+
<p class="applet-canvas-progress-line">
|
|
302
|
+
<span
|
|
303
|
+
v-if="progress.working"
|
|
304
|
+
class="applet-canvas-progress-dot"
|
|
305
|
+
aria-hidden="true"
|
|
306
|
+
/>
|
|
307
|
+
<UiIcon v-else-if="progress.failure" name="close" size="sm" />
|
|
308
|
+
<span>{{ progress.label }}</span>
|
|
309
|
+
</p>
|
|
310
|
+
<p v-if="progress.failure" class="applet-canvas-progress-failure">
|
|
311
|
+
{{ progress.failure }}
|
|
312
|
+
</p>
|
|
313
|
+
<pre
|
|
314
|
+
v-if="progress.output"
|
|
315
|
+
class="applet-canvas-progress-output"
|
|
316
|
+
><code>{{ progress.output.join("\n") }}</code></pre>
|
|
317
|
+
</section>
|
|
318
|
+
|
|
258
319
|
<div class="applet-canvas-body">
|
|
259
320
|
<!-- Loading: the shape of what is coming, with a caption that says so. -->
|
|
260
321
|
<div v-if="loading" class="applet-canvas-loading">
|
|
@@ -297,21 +358,6 @@ async function clearFocus(): Promise<void> {
|
|
|
297
358
|
<span>This is taking longer than it should.</span>
|
|
298
359
|
<button type="button" @click="retry">Try again</button>
|
|
299
360
|
</div>
|
|
300
|
-
<p
|
|
301
|
-
v-if="build && build.status !== 'unknown'"
|
|
302
|
-
class="applet-canvas-build"
|
|
303
|
-
:class="`applet-canvas-build-${build.status}`"
|
|
304
|
-
>
|
|
305
|
-
<UiIcon
|
|
306
|
-
:name="build.status === 'passed' ? 'check' : 'close'"
|
|
307
|
-
size="sm"
|
|
308
|
-
/>
|
|
309
|
-
<span
|
|
310
|
-
>{{ build.command === "build" ? "Build" : "Check" }}
|
|
311
|
-
{{ build.status
|
|
312
|
-
}}{{ build.summary ? `: ${build.summary}` : "" }}</span
|
|
313
|
-
>
|
|
314
|
-
</p>
|
|
315
361
|
<div v-if="sortedFiles.length > 0" class="applet-canvas-files">
|
|
316
362
|
<button
|
|
317
363
|
v-for="file in sortedFiles"
|
|
@@ -573,28 +619,76 @@ async function clearFocus(): Promise<void> {
|
|
|
573
619
|
white-space: pre;
|
|
574
620
|
}
|
|
575
621
|
|
|
576
|
-
.
|
|
622
|
+
/* Where the work has got to: the panel's own header for the building state. */
|
|
623
|
+
.applet-canvas-progress {
|
|
624
|
+
display: flex;
|
|
625
|
+
flex: 0 0 auto;
|
|
626
|
+
flex-direction: column;
|
|
627
|
+
gap: 8px;
|
|
628
|
+
padding: 10px 12px;
|
|
629
|
+
border-bottom: 1px solid var(--frock-border);
|
|
630
|
+
background: var(--frock-surface-subtle);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.applet-canvas-progress-wrong {
|
|
634
|
+
border-bottom-color: var(--frock-danger-border);
|
|
635
|
+
background: var(--frock-danger-surface);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.applet-canvas-progress-line {
|
|
577
639
|
display: flex;
|
|
578
640
|
align-items: center;
|
|
579
|
-
gap:
|
|
641
|
+
gap: 8px;
|
|
580
642
|
margin: 0;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
643
|
+
color: var(--frock-text);
|
|
644
|
+
font-size: var(--frock-text-sm);
|
|
645
|
+
font-weight: 600;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.applet-canvas-progress-wrong .applet-canvas-progress-line {
|
|
649
|
+
color: var(--frock-danger-text);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* The same "something is happening" the thread uses, at the size of a word. */
|
|
653
|
+
.applet-canvas-progress-dot {
|
|
654
|
+
width: 8px;
|
|
655
|
+
height: 8px;
|
|
656
|
+
flex: 0 0 auto;
|
|
657
|
+
border-radius: 999px;
|
|
658
|
+
background: var(--frock-action-primary);
|
|
659
|
+
animation: applet-progress-pulse 1.4s ease-in-out infinite;
|
|
586
660
|
}
|
|
587
661
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
662
|
+
@keyframes applet-progress-pulse {
|
|
663
|
+
0%,
|
|
664
|
+
100% {
|
|
665
|
+
opacity: 0.35;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
50% {
|
|
669
|
+
opacity: 1;
|
|
670
|
+
}
|
|
592
671
|
}
|
|
593
672
|
|
|
594
|
-
.applet-canvas-
|
|
595
|
-
|
|
673
|
+
.applet-canvas-progress-failure {
|
|
674
|
+
margin: 0;
|
|
596
675
|
color: var(--frock-danger-text);
|
|
597
|
-
|
|
676
|
+
font-size: var(--frock-text-sm);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.applet-canvas-progress-output {
|
|
680
|
+
max-height: 180px;
|
|
681
|
+
margin: 0;
|
|
682
|
+
overflow: auto;
|
|
683
|
+
padding: 8px 10px;
|
|
684
|
+
border: 1px solid var(--frock-border);
|
|
685
|
+
border-radius: var(--frock-radius-control);
|
|
686
|
+
color: var(--frock-text-muted);
|
|
687
|
+
background: var(--frock-surface);
|
|
688
|
+
font-family: var(--frock-font-mono);
|
|
689
|
+
font-size: var(--frock-text-xs);
|
|
690
|
+
line-height: var(--frock-leading-snug);
|
|
691
|
+
white-space: pre;
|
|
598
692
|
}
|
|
599
693
|
|
|
600
694
|
.applet-canvas-failure {
|
|
@@ -686,5 +780,9 @@ async function clearFocus(): Promise<void> {
|
|
|
686
780
|
.applet-app-leave-active {
|
|
687
781
|
transition: none;
|
|
688
782
|
}
|
|
783
|
+
|
|
784
|
+
.applet-canvas-progress-dot {
|
|
785
|
+
animation: none;
|
|
786
|
+
}
|
|
689
787
|
}
|
|
690
788
|
</style>
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from "./turn-limits.js";
|
|
57
57
|
import SendPayloadView from "./SendPayloadView.vue";
|
|
58
58
|
import AppletCanvas from "./AppletCanvas.vue";
|
|
59
|
+
import { appletProgressToolsV1, appletProgressV1 } from "./applet-progress.js";
|
|
59
60
|
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
60
61
|
import type { ClientSkillCatalogEntryV1 } from "../skill-protocol.js";
|
|
61
62
|
import {
|
|
@@ -67,6 +68,14 @@ import {
|
|
|
67
68
|
textWithoutSkillTriggerV1,
|
|
68
69
|
type SkillPopoverStateV1,
|
|
69
70
|
} from "./skill-invocation.js";
|
|
71
|
+
import {
|
|
72
|
+
DEPLOYMENT_RELOAD_LABEL_V1,
|
|
73
|
+
DEPLOYMENT_UPDATED_MESSAGE_V1,
|
|
74
|
+
deploymentFollowV1,
|
|
75
|
+
deploymentReloadStoreV1,
|
|
76
|
+
readDeploymentReloadV1,
|
|
77
|
+
writeDeploymentReloadV1,
|
|
78
|
+
} from "./deployment.js";
|
|
70
79
|
|
|
71
80
|
const injectedWeb = inject(frockBotWebDataKey);
|
|
72
81
|
if (!injectedWeb) throw new Error("shell client data was not provided");
|
|
@@ -264,6 +273,25 @@ const appletChip = computed(() =>
|
|
|
264
273
|
? (state.value.focusedApplet?.displayName ?? "Applet")
|
|
265
274
|
: undefined,
|
|
266
275
|
);
|
|
276
|
+
/*
|
|
277
|
+
* What the Bot is doing to it, on the phone.
|
|
278
|
+
*
|
|
279
|
+
* There is no room beside the conversation here, so the chip carries the line
|
|
280
|
+
* the canvas would have shown and opens the canvas over the whole screen. It
|
|
281
|
+
* is the same projection and the same words: a person who moves between their
|
|
282
|
+
* phone and a laptop reads one story, not two.
|
|
283
|
+
*/
|
|
284
|
+
const appletChipStatus = computed(() => {
|
|
285
|
+
if (!appletChip.value) return undefined;
|
|
286
|
+
const progress = appletProgressV1({
|
|
287
|
+
applet: state.value.focusedApplet ?? null,
|
|
288
|
+
source: state.value.appletSource,
|
|
289
|
+
build: state.value.appletBuild,
|
|
290
|
+
tools: appletProgressToolsV1(state.value.messages),
|
|
291
|
+
running: Boolean(state.value.activeRunId),
|
|
292
|
+
});
|
|
293
|
+
return progress && progress.stage !== "published" ? progress : undefined;
|
|
294
|
+
});
|
|
267
295
|
/*
|
|
268
296
|
* Skill invocation. `/` or `@` at a word boundary opens a popover over the
|
|
269
297
|
* Bot's catalog; choosing one attaches a ref chip and removes the trigger from
|
|
@@ -411,6 +439,57 @@ let voiceTail = "";
|
|
|
411
439
|
let voiceSendOnFinal = false;
|
|
412
440
|
|
|
413
441
|
const dictating = computed(() => voiceState.value !== "idle");
|
|
442
|
+
|
|
443
|
+
/*
|
|
444
|
+
* Following a release.
|
|
445
|
+
*
|
|
446
|
+
* A reload is destructive, so the shell only does it on its own when there is
|
|
447
|
+
* nothing to lose, and otherwise says so and waits. The bar is not an alert:
|
|
448
|
+
* nothing is wrong, there is just newer code to pick up.
|
|
449
|
+
*/
|
|
450
|
+
const reloadStore = deploymentReloadStoreV1();
|
|
451
|
+
const lastReloadedAt = readDeploymentReloadV1(reloadStore);
|
|
452
|
+
const updateBarVisible = ref(false);
|
|
453
|
+
|
|
454
|
+
function followDeployment(): void {
|
|
455
|
+
const decision = deploymentFollowV1({
|
|
456
|
+
stale: state.value.deploymentStale,
|
|
457
|
+
turnRunning: Boolean(state.value.runningRunId),
|
|
458
|
+
draft: draft.value,
|
|
459
|
+
overlayOpen: Boolean(overlaySurface.value),
|
|
460
|
+
listening: dictating.value,
|
|
461
|
+
holds: state.value.reloadHolds,
|
|
462
|
+
now: Date.now(),
|
|
463
|
+
...(lastReloadedAt === undefined ? {} : { reloadedAt: lastReloadedAt }),
|
|
464
|
+
});
|
|
465
|
+
updateBarVisible.value = decision === "offer";
|
|
466
|
+
if (decision !== "reload") return;
|
|
467
|
+
reloadNow();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function reloadNow(): void {
|
|
471
|
+
writeDeploymentReloadV1(reloadStore, Date.now());
|
|
472
|
+
/*
|
|
473
|
+
* `location.reload()` and nothing else. The Android shell runs this very
|
|
474
|
+
* bundle inside a WebView served from its own origin, so re-navigating to a
|
|
475
|
+
* URL this code built would leave that origin behind; reloading in place
|
|
476
|
+
* works the same way in both.
|
|
477
|
+
*/
|
|
478
|
+
window.location.reload();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
watch(
|
|
482
|
+
[
|
|
483
|
+
() => state.value.deploymentStale,
|
|
484
|
+
() => state.value.runningRunId,
|
|
485
|
+
() => state.value.reloadHolds,
|
|
486
|
+
draft,
|
|
487
|
+
overlaySurface,
|
|
488
|
+
dictating,
|
|
489
|
+
],
|
|
490
|
+
() => followDeployment(),
|
|
491
|
+
{ immediate: true },
|
|
492
|
+
);
|
|
414
493
|
const voiceButtonLabel = computed(() => voiceButtonLabelV1(voiceState.value));
|
|
415
494
|
/**
|
|
416
495
|
* The wave button takes the slot only when the slot is otherwise idle: an
|
|
@@ -1574,6 +1653,23 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
1574
1653
|
</div>
|
|
1575
1654
|
</Transition>
|
|
1576
1655
|
|
|
1656
|
+
<Transition name="banner">
|
|
1657
|
+
<!--
|
|
1658
|
+
One bar at a time in this spot. A failed Turn is the more urgent
|
|
1659
|
+
thing to read, and newer code is still there once it is dealt with.
|
|
1660
|
+
-->
|
|
1661
|
+
<div
|
|
1662
|
+
v-if="updateBarVisible && !state.error && !state.activeRun"
|
|
1663
|
+
class="update-banner"
|
|
1664
|
+
role="status"
|
|
1665
|
+
>
|
|
1666
|
+
<span>{{ DEPLOYMENT_UPDATED_MESSAGE_V1 }}</span>
|
|
1667
|
+
<button type="button" @click="reloadNow()">
|
|
1668
|
+
{{ DEPLOYMENT_RELOAD_LABEL_V1 }}
|
|
1669
|
+
</button>
|
|
1670
|
+
</div>
|
|
1671
|
+
</Transition>
|
|
1672
|
+
|
|
1577
1673
|
<!--
|
|
1578
1674
|
No Bot, no composer. A disabled input under a made-up Bot name reads
|
|
1579
1675
|
as a broken Bot; the first-run pane above points at making one.
|
|
@@ -1618,11 +1714,24 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
1618
1714
|
v-if="appletChip"
|
|
1619
1715
|
type="button"
|
|
1620
1716
|
class="applet-chip"
|
|
1717
|
+
data-testid="applet-chip"
|
|
1621
1718
|
@click="toggleRightPanel"
|
|
1622
1719
|
>
|
|
1623
1720
|
<UiIcon name="applets" size="sm" />
|
|
1624
|
-
<span class="applet-chip-
|
|
1625
|
-
|
|
1721
|
+
<span class="applet-chip-text">
|
|
1722
|
+
<span class="applet-chip-name">Applet: {{ appletChip }}</span>
|
|
1723
|
+
<span v-if="appletChipStatus" class="applet-chip-status">
|
|
1724
|
+
<span
|
|
1725
|
+
v-if="appletChipStatus.working"
|
|
1726
|
+
class="applet-chip-dot"
|
|
1727
|
+
aria-hidden="true"
|
|
1728
|
+
/>
|
|
1729
|
+
{{ appletChipStatus.failure ?? appletChipStatus.label }}
|
|
1730
|
+
</span>
|
|
1731
|
+
</span>
|
|
1732
|
+
<span class="applet-chip-action">{{
|
|
1733
|
+
appletChipStatus ? "Watch" : "Open"
|
|
1734
|
+
}}</span>
|
|
1626
1735
|
</button>
|
|
1627
1736
|
<div class="composer-body">
|
|
1628
1737
|
<ul v-if="attachedSkills.length > 0" class="skill-chips">
|