@frockbot/plugin-shell 0.3.4 → 0.3.6
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 +1 -1
- package/package.json +31 -31
- package/src/backend-authoring.test.ts +72 -0
- package/src/backend-authoring.ts +82 -16
- package/src/backend-configuration.test.ts +6 -4
- package/src/backend-package-catalog.test.ts +23 -3
- package/src/backend-package-catalog.ts +7 -1
- package/src/backend-recovery-integration.test.ts +23 -23
- package/src/backend.ts +119 -29
- package/src/client/AppletCanvas.vue +3 -5
- package/src/client/FrockBotApp.vue +197 -79
- package/src/client/PackageIframeHost.vue +15 -6
- package/src/client/index.test.ts +111 -15
- package/src/client/index.ts +217 -43
- package/src/client/model-presentation.test.ts +4 -2
- package/src/client/model-presentation.ts +2 -2
- package/src/client/styles.css +76 -1
- package/src/run-protocol.test.ts +73 -0
- package/src/run-protocol.ts +161 -9
- package/src/shared.ts +6 -0
package/src/backend.ts
CHANGED
|
@@ -328,7 +328,11 @@ import {
|
|
|
328
328
|
type PendingBotInputV1,
|
|
329
329
|
type RoutineInboxEntryV1,
|
|
330
330
|
} from "@frockbot/plugin-routines/inbox";
|
|
331
|
-
import type {
|
|
331
|
+
import type {
|
|
332
|
+
RoutineFireOutcomeV1,
|
|
333
|
+
RoutineScheduler,
|
|
334
|
+
} from "@frockbot/plugin-routines/scheduler";
|
|
335
|
+
import type { RoutineFireV1 } from "@frockbot/plugin-routines/firing";
|
|
332
336
|
import {
|
|
333
337
|
RoutineNotFoundError,
|
|
334
338
|
type RoutineStore,
|
|
@@ -395,6 +399,7 @@ import {
|
|
|
395
399
|
isVisibleRunV1,
|
|
396
400
|
projectClientRunLookupV1,
|
|
397
401
|
projectClientRunV1,
|
|
402
|
+
projectClientRunOrDegradedV1,
|
|
398
403
|
projectClientAnnouncementsV1,
|
|
399
404
|
projectClientTurnV1,
|
|
400
405
|
type ClientRunLookupV1,
|
|
@@ -441,6 +446,25 @@ import {
|
|
|
441
446
|
} from "./unread.js";
|
|
442
447
|
import { defineBotBackendContribution } from "@frockbot/kernel-contracts/contributions";
|
|
443
448
|
|
|
449
|
+
/**
|
|
450
|
+
* The providers that can be asked what happened to a model request they never
|
|
451
|
+
* answered — that is, the ones whose Package registers an
|
|
452
|
+
* `LlmReconciliationCapability` (ADR 0028).
|
|
453
|
+
*
|
|
454
|
+
* It is a list rather than a lookup because recovery consults it inside the
|
|
455
|
+
* durable transaction that settles the run, where nothing may be mounted or
|
|
456
|
+
* awaited. The cost is that a provider Package which gains retrieval has to
|
|
457
|
+
* name itself here; the ADR carries that obligation, and the failure mode of
|
|
458
|
+
* forgetting is a Turn settled as failed rather than one wedged forever, which
|
|
459
|
+
* is the direction this deployment wants to be wrong in.
|
|
460
|
+
*
|
|
461
|
+
* Today: the in-process foundation provider, and nothing else. Ollama Cloud
|
|
462
|
+
* exposes no provider-bound retrieval (ADR 0010) and neither does Flock AI.
|
|
463
|
+
*/
|
|
464
|
+
const RECONCILING_PROVIDER_IDS_V1: ReadonlySet<string> = new Set([
|
|
465
|
+
"foundation",
|
|
466
|
+
]);
|
|
467
|
+
|
|
444
468
|
export const BOT_CONFIGURATION_KEY = "bot-configuration";
|
|
445
469
|
const CONFIGURATION_RECEIPT_PREFIX = "configuration-receipt:";
|
|
446
470
|
const STOP_RECEIPT_PREFIX = "stop-receipt:";
|
|
@@ -779,6 +803,8 @@ export class ShellBotBackendContribution {
|
|
|
779
803
|
deferScheduledWork: (transaction) =>
|
|
780
804
|
this.deferScheduledWork(transaction),
|
|
781
805
|
settleScheduledWork: () => this.settleScheduledWork(),
|
|
806
|
+
providerReconciles: (providerId) =>
|
|
807
|
+
RECONCILING_PROVIDER_IDS_V1.has(providerId),
|
|
782
808
|
},
|
|
783
809
|
});
|
|
784
810
|
}
|
|
@@ -2975,17 +3001,24 @@ export class ShellBotBackendContribution {
|
|
|
2975
3001
|
}
|
|
2976
3002
|
|
|
2977
3003
|
private async settleScheduledWork(): Promise<void> {
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
this.
|
|
2988
|
-
|
|
3004
|
+
// The re-arm is in a `finally` because it is the object's only way back.
|
|
3005
|
+
// The alarm that woke this object has already been consumed by the
|
|
3006
|
+
// platform; a throw in any one settler used to skip the re-arm, and then
|
|
3007
|
+
// nothing — no Routine, no approval expiry, no owed subagent Turn — ever
|
|
3008
|
+
// woke this Bot again except by a caller's luck. One producer failing must
|
|
3009
|
+
// cost that producer its pass, never the clock.
|
|
3010
|
+
try {
|
|
3011
|
+
await this.settleRoutineFirings();
|
|
3012
|
+
await this.runOwedSubagentTurns();
|
|
3013
|
+
await this.reconcileOverdueTasks();
|
|
3014
|
+
await this.expireDueApprovals();
|
|
3015
|
+
await this.replayPendingWakeNotifications();
|
|
3016
|
+
await this.hostSettleScheduledWork?.();
|
|
3017
|
+
} finally {
|
|
3018
|
+
await this.ctx.storage.transaction((transaction) =>
|
|
3019
|
+
this.authority.refreshRecoveryAlarm(transaction),
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
2989
3022
|
}
|
|
2990
3023
|
|
|
2991
3024
|
/**
|
|
@@ -3005,21 +3038,73 @@ export class ShellBotBackendContribution {
|
|
|
3005
3038
|
// here whenever the Turn is executing in this isolate, but a durable active
|
|
3006
3039
|
// run outlives an eviction, and admitting a firing against one would burn
|
|
3007
3040
|
// the occurrence on an error instead of holding the debt.
|
|
3008
|
-
|
|
3041
|
+
//
|
|
3042
|
+
// Returning was not enough: the debt stayed past-due, so `deadlines()`
|
|
3043
|
+
// re-armed on a moment already gone and the alarm spun straight back into
|
|
3044
|
+
// this same bail-out — which is how a Routine racing a long chat Turn
|
|
3045
|
+
// failed once a minute for ever. The hold is what turns the bail-out into
|
|
3046
|
+
// a deferral: `dueAt` does not move, so the firing still lands.
|
|
3047
|
+
if (await this.authority.readActiveRunId()) {
|
|
3048
|
+
await this.ctx.storage.transaction((transaction) =>
|
|
3049
|
+
this.routineScheduler.defer(transaction),
|
|
3050
|
+
);
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3009
3053
|
await this.routineScheduler.settle(async (fire) => {
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3054
|
+
const outcome = await this.runOneFiring(identity, fire);
|
|
3055
|
+
await this.notifyFailedFiring(identity, fire, outcome);
|
|
3056
|
+
return outcome;
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
private async runOneFiring(
|
|
3061
|
+
identity: BotIdentity,
|
|
3062
|
+
fire: RoutineFireV1,
|
|
3063
|
+
): Promise<RoutineFireOutcomeV1> {
|
|
3064
|
+
try {
|
|
3065
|
+
await this.authority.run(
|
|
3066
|
+
routineTurnCommandV1(identity, fire, new Date().toISOString()),
|
|
3067
|
+
);
|
|
3068
|
+
} catch (error) {
|
|
3020
3069
|
return routineFireOutcomeV1(
|
|
3021
3070
|
await this.authority.readStoredRun(fire.fireId),
|
|
3071
|
+
error,
|
|
3022
3072
|
);
|
|
3073
|
+
}
|
|
3074
|
+
return routineFireOutcomeV1(
|
|
3075
|
+
await this.authority.readStoredRun(fire.fireId),
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* Tell the person that a firing did not work.
|
|
3081
|
+
*
|
|
3082
|
+
* The scheduler has already written the durable completion-inbox entry in
|
|
3083
|
+
* the transaction that settled the firing; this is the delivery half — the
|
|
3084
|
+
* same seam a hand-off uses, so a Routine that breaks reaches the same place
|
|
3085
|
+
* a Routine that finishes does instead of only a `failed` row nobody opens.
|
|
3086
|
+
* `notifications.enabled` is honoured: it is the mute on updates, and a
|
|
3087
|
+
* broken Routine is an update, not a decision the Bot is waiting on.
|
|
3088
|
+
*/
|
|
3089
|
+
private async notifyFailedFiring(
|
|
3090
|
+
identity: BotIdentity,
|
|
3091
|
+
fire: RoutineFireV1,
|
|
3092
|
+
outcome: RoutineFireOutcomeV1,
|
|
3093
|
+
): Promise<void> {
|
|
3094
|
+
if (outcome.status === "ok") return;
|
|
3095
|
+
const settings = await this.getSettings(identity);
|
|
3096
|
+
if (!settings.notifications.enabled) return;
|
|
3097
|
+
await this.authority.recordNotification({
|
|
3098
|
+
// The same id shape the completion path uses, so one firing is one
|
|
3099
|
+
// intent however many times the alarm retries it.
|
|
3100
|
+
notificationId: `routine-failed:${fire.fireId}`,
|
|
3101
|
+
runId: fire.fireId,
|
|
3102
|
+
createdAt: new Date().toISOString(),
|
|
3103
|
+
title: `${settings.profile.name} could not run a Routine`,
|
|
3104
|
+
body: (outcome.summary ?? "The firing ended without saying why.").slice(
|
|
3105
|
+
0,
|
|
3106
|
+
240,
|
|
3107
|
+
),
|
|
3023
3108
|
});
|
|
3024
3109
|
}
|
|
3025
3110
|
// -------------------------------------------------------------------------
|
|
@@ -4508,13 +4593,13 @@ export class ShellBotBackendContribution {
|
|
|
4508
4593
|
if (!effectiveModel) {
|
|
4509
4594
|
throw new Error(
|
|
4510
4595
|
effective.binding?.failure ??
|
|
4511
|
-
"No model is
|
|
4596
|
+
"No model is set up yet. Choose one in Models.",
|
|
4512
4597
|
);
|
|
4513
4598
|
}
|
|
4514
4599
|
const binding: ResolvedModelBindingV1 = effective.binding ?? {
|
|
4515
4600
|
model: structuredClone(effectiveModel),
|
|
4516
4601
|
state: "unavailable",
|
|
4517
|
-
failure: "
|
|
4602
|
+
failure: "This Bot's model isn't available. Pick one in Models.",
|
|
4518
4603
|
};
|
|
4519
4604
|
if (
|
|
4520
4605
|
binding.state === "unavailable" ||
|
|
@@ -4523,7 +4608,8 @@ export class ShellBotBackendContribution {
|
|
|
4523
4608
|
!binding.packageId
|
|
4524
4609
|
) {
|
|
4525
4610
|
throw new Error(
|
|
4526
|
-
binding.failure ??
|
|
4611
|
+
binding.failure ??
|
|
4612
|
+
"This Bot's model isn't available. Pick one in Models.",
|
|
4527
4613
|
);
|
|
4528
4614
|
}
|
|
4529
4615
|
if (
|
|
@@ -4536,7 +4622,9 @@ export class ShellBotBackendContribution {
|
|
|
4536
4622
|
admittedRequest.modelBinding.connectionGeneration !==
|
|
4537
4623
|
binding.connection.generation)
|
|
4538
4624
|
) {
|
|
4539
|
-
throw new Error(
|
|
4625
|
+
throw new Error(
|
|
4626
|
+
"This Bot's model changed mid-reply. Send your message again.",
|
|
4627
|
+
);
|
|
4540
4628
|
}
|
|
4541
4629
|
const bindingPackageId = binding.packageId;
|
|
4542
4630
|
agentPackages.push(
|
|
@@ -5370,7 +5458,9 @@ export class ShellBotBackendContribution {
|
|
|
5370
5458
|
// still not part of the conversation: the visible transcript never
|
|
5371
5459
|
// shows one, running or settled.
|
|
5372
5460
|
if (active && isVisibleRunV1(active))
|
|
5373
|
-
selected.set(active.runId, {
|
|
5461
|
+
selected.set(active.runId, {
|
|
5462
|
+
run: projectClientRunOrDegradedV1(active),
|
|
5463
|
+
});
|
|
5374
5464
|
}
|
|
5375
5465
|
const available = candidates.slice(0, CLIENT_RUN_PAGE_LIMIT);
|
|
5376
5466
|
let stoppedEarly = false;
|
|
@@ -5382,7 +5472,7 @@ export class ShellBotBackendContribution {
|
|
|
5382
5472
|
}
|
|
5383
5473
|
const stored = await this.authority.readStoredRun(candidate.runId);
|
|
5384
5474
|
if (!stored || !isVisibleRunV1(stored)) continue;
|
|
5385
|
-
const projected =
|
|
5475
|
+
const projected = projectClientRunOrDegradedV1(stored);
|
|
5386
5476
|
const tentative = [
|
|
5387
5477
|
...selected.values(),
|
|
5388
5478
|
{ cursor: candidate.cursor, run: projected },
|
|
@@ -147,7 +147,7 @@ function generationLabel(generationId: string): string {
|
|
|
147
147
|
hour: "numeric",
|
|
148
148
|
minute: "2-digit",
|
|
149
149
|
});
|
|
150
|
-
return `Live since ${when}
|
|
150
|
+
return `Live since ${when}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function selectTab(next: "app" | "code"): void {
|
|
@@ -199,10 +199,8 @@ async function clearFocus(): Promise<void> {
|
|
|
199
199
|
/></span>
|
|
200
200
|
<div class="applet-canvas-title">
|
|
201
201
|
<strong>{{ applet?.displayName ?? "Applet" }}</strong>
|
|
202
|
-
<small v-if="viewer"
|
|
203
|
-
|
|
204
|
-
}}</small>
|
|
205
|
-
<small v-else>No published version yet</small>
|
|
202
|
+
<small v-if="viewer">{{ generationLabel(viewer.generationId) }}</small>
|
|
203
|
+
<small v-else>Not published yet</small>
|
|
206
204
|
</div>
|
|
207
205
|
<div
|
|
208
206
|
v-if="canShowApp"
|
|
@@ -288,7 +288,7 @@ const threadHeading = computed(() => {
|
|
|
288
288
|
const threadHint = computed(() => {
|
|
289
289
|
if (!hasBot.value) return "Add your first sheep to start a conversation.";
|
|
290
290
|
if (state.value.modelReady) {
|
|
291
|
-
return "
|
|
291
|
+
return "Say anything to get started.";
|
|
292
292
|
}
|
|
293
293
|
return state.value.modelLabel;
|
|
294
294
|
});
|
|
@@ -405,6 +405,69 @@ function taskChipsOf(message: WebChatMessage): Array<{
|
|
|
405
405
|
});
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* The tools this Turn ran, as the thread draws them.
|
|
410
|
+
*
|
|
411
|
+
* A tool whose Package draws its own surface is shown by that surface; every
|
|
412
|
+
* other one is a chip, because a Turn that spends a minute making tool calls
|
|
413
|
+
* used to show the User nothing at all but a spinning avatar.
|
|
414
|
+
*/
|
|
415
|
+
function toolChipsOf(message: WebChatMessage): WebToolActivity[] {
|
|
416
|
+
return message.tools.filter((tool) => iframeEntriesFor(tool).length === 0);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* What a chip calls a tool, in the User's words rather than the model's.
|
|
421
|
+
*
|
|
422
|
+
* A tool name is an identifier — `send_to_user`, `user-Github--acme/search_issues`
|
|
423
|
+
* — and the transcript is a conversation, so the chip drops the namespace,
|
|
424
|
+
* un-snakes the rest and capitalises it.
|
|
425
|
+
*/
|
|
426
|
+
function toolChipLabel(tool: WebToolActivity): string {
|
|
427
|
+
const bare = tool.name.split("/").pop() ?? tool.name;
|
|
428
|
+
const words = bare.replace(/[_.-]+/g, " ").trim();
|
|
429
|
+
if (words.length === 0) return tool.name;
|
|
430
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Whether a chip is drawn as a failure.
|
|
435
|
+
*
|
|
436
|
+
* A tool call the model recovered from is not a failure the User has anything
|
|
437
|
+
* to do with: a refused call followed by a Turn that went on to finish is the
|
|
438
|
+
* Bot correcting itself, and colouring it red reports a broken Turn that
|
|
439
|
+
* worked. Only a Turn that itself ended badly keeps the failed state.
|
|
440
|
+
*/
|
|
441
|
+
function toolChipState(
|
|
442
|
+
tool: WebToolActivity,
|
|
443
|
+
message: WebChatMessage,
|
|
444
|
+
): "running" | "completed" | "failed" | "retried" {
|
|
445
|
+
if (tool.status !== "failed") return tool.status;
|
|
446
|
+
return message.status === "completed" || message.status === "streaming"
|
|
447
|
+
? "retried"
|
|
448
|
+
: "failed";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** What a chip says a tool is doing. Its status, in the User's words. */
|
|
452
|
+
function toolChipStatus(
|
|
453
|
+
tool: WebToolActivity,
|
|
454
|
+
message: WebChatMessage,
|
|
455
|
+
): string {
|
|
456
|
+
const state = toolChipState(tool, message);
|
|
457
|
+
if (state === "running") return "running";
|
|
458
|
+
if (state === "retried") return "retried";
|
|
459
|
+
return state === "failed" ? "failed" : "done";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Which tool chips the User has opened. Local, and per chip. */
|
|
463
|
+
const expandedTools = ref(new Set<string>());
|
|
464
|
+
|
|
465
|
+
function toggleTool(toolId: string): void {
|
|
466
|
+
const next = new Set(expandedTools.value);
|
|
467
|
+
if (!next.delete(toolId)) next.add(toolId);
|
|
468
|
+
expandedTools.value = next;
|
|
469
|
+
}
|
|
470
|
+
|
|
408
471
|
/** Which chips the User has opened. Local, and per chip. */
|
|
409
472
|
const expandedTasks = ref(new Set<string>());
|
|
410
473
|
|
|
@@ -431,6 +494,7 @@ function isVisible(message: WebChatMessage): boolean {
|
|
|
431
494
|
// visible act was dispatching a subagent.
|
|
432
495
|
return (
|
|
433
496
|
message.text.length > 0 ||
|
|
497
|
+
(message.notice?.length ?? 0) > 0 ||
|
|
434
498
|
message.tools.some((tool) => iframeEntriesFor(tool).length > 0) ||
|
|
435
499
|
message.sends.length > 0 ||
|
|
436
500
|
(message.tasks?.length ?? 0) > 0 ||
|
|
@@ -856,105 +920,159 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
856
920
|
/></span>
|
|
857
921
|
<k-slot name="frockbot.bot-avatar" />
|
|
858
922
|
</div>
|
|
859
|
-
<div v-if="message.text" class="message-bubble">
|
|
860
|
-
<UiMarkdown :text="message.text" />
|
|
861
|
-
</div>
|
|
862
|
-
<template v-for="tool in message.tools" :key="tool.id">
|
|
863
|
-
<PackageIframeHost
|
|
864
|
-
v-for="entry in iframeEntriesFor(tool)"
|
|
865
|
-
:key="`${tool.id}:${entry.contribution.packageId}:${entry.page.id}`"
|
|
866
|
-
class="message-package-iframe"
|
|
867
|
-
:contribution="entry.contribution"
|
|
868
|
-
:page="entry.page"
|
|
869
|
-
:slot="entry.slot"
|
|
870
|
-
:states="{ [`tool:${tool.name}`]: toolResultState(tool) }"
|
|
871
|
-
/>
|
|
872
|
-
</template>
|
|
873
923
|
<!--
|
|
924
|
+
Everything the Turn produced stacks in one column beside the
|
|
925
|
+
avatar. The row holds exactly two children — avatar, column —
|
|
926
|
+
so a bubble, a notice and a chip are stacked lines rather than
|
|
927
|
+
side-by-side columns squeezing the reply to a few pixels.
|
|
928
|
+
-->
|
|
929
|
+
<div class="message-column">
|
|
930
|
+
<div v-if="message.text" class="message-bubble">
|
|
931
|
+
<UiMarkdown :text="message.text" />
|
|
932
|
+
</div>
|
|
933
|
+
<!--
|
|
934
|
+
Why the Turn ends where it does, under whatever it had already
|
|
935
|
+
said rather than in place of it.
|
|
936
|
+
-->
|
|
937
|
+
<p v-if="message.notice" class="message-notice">
|
|
938
|
+
{{ message.notice }}
|
|
939
|
+
</p>
|
|
940
|
+
<template v-for="tool in message.tools" :key="tool.id">
|
|
941
|
+
<PackageIframeHost
|
|
942
|
+
v-for="entry in iframeEntriesFor(tool)"
|
|
943
|
+
:key="`${tool.id}:${entry.contribution.packageId}:${entry.page.id}`"
|
|
944
|
+
class="message-package-iframe"
|
|
945
|
+
:contribution="entry.contribution"
|
|
946
|
+
:page="entry.page"
|
|
947
|
+
:slot="entry.slot"
|
|
948
|
+
:states="{ [`tool:${tool.name}`]: toolResultState(tool) }"
|
|
949
|
+
/>
|
|
950
|
+
</template>
|
|
951
|
+
<!--
|
|
874
952
|
A binary a tool filed in the Workspace, drawn from the
|
|
875
953
|
Workspace read route. The thread carries the path, never the
|
|
876
954
|
bytes, so a long conversation costs paths and the image is
|
|
877
955
|
fetched only when it is on screen.
|
|
878
956
|
-->
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
>
|
|
883
|
-
<a
|
|
884
|
-
v-for="attachment in attachmentsOf(message)"
|
|
885
|
-
:key="attachment.contentHash"
|
|
886
|
-
:href="workspaceFileUrl(attachment.path)"
|
|
887
|
-
target="_blank"
|
|
888
|
-
rel="noreferrer"
|
|
957
|
+
<div
|
|
958
|
+
v-if="attachmentsOf(message).length > 0"
|
|
959
|
+
class="message-attachments"
|
|
889
960
|
>
|
|
890
|
-
<
|
|
891
|
-
|
|
892
|
-
:
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
961
|
+
<a
|
|
962
|
+
v-for="attachment in attachmentsOf(message)"
|
|
963
|
+
:key="attachment.contentHash"
|
|
964
|
+
:href="workspaceFileUrl(attachment.path)"
|
|
965
|
+
target="_blank"
|
|
966
|
+
rel="noreferrer"
|
|
967
|
+
>
|
|
968
|
+
<img
|
|
969
|
+
:src="workspaceFileUrl(attachment.path)"
|
|
970
|
+
:alt="`Attachment from ${message.runId}`"
|
|
971
|
+
loading="lazy"
|
|
972
|
+
/>
|
|
973
|
+
</a>
|
|
974
|
+
</div>
|
|
975
|
+
<!--
|
|
898
976
|
Sends sit beside the derived text rather than inside it: each
|
|
899
977
|
payload is its own block, and a widget-ended Turn has no text
|
|
900
978
|
bubble at all.
|
|
901
979
|
-->
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
980
|
+
<div v-if="message.sends.length > 0" class="message-sends">
|
|
981
|
+
<SendPayloadView
|
|
982
|
+
v-for="(send, sendIndex) in message.sends"
|
|
983
|
+
:key="sendIndex"
|
|
984
|
+
:send="send"
|
|
985
|
+
/>
|
|
986
|
+
</div>
|
|
987
|
+
<!--
|
|
988
|
+
What the Bot did, while it is doing it. The chip is the
|
|
989
|
+
conversation's whole account of an ordinary tool call: its
|
|
990
|
+
name, whether it is running, and — when the User opens it —
|
|
991
|
+
what it returned.
|
|
992
|
+
-->
|
|
993
|
+
<div
|
|
994
|
+
v-if="toolChipsOf(message).length > 0"
|
|
995
|
+
class="message-tools"
|
|
996
|
+
>
|
|
997
|
+
<button
|
|
998
|
+
v-for="tool in toolChipsOf(message)"
|
|
999
|
+
:key="tool.id"
|
|
1000
|
+
type="button"
|
|
1001
|
+
class="tool-chip"
|
|
1002
|
+
:class="`tool-chip-${toolChipState(tool, message)}`"
|
|
1003
|
+
:aria-expanded="expandedTools.has(tool.id)"
|
|
1004
|
+
@click="toggleTool(tool.id)"
|
|
1005
|
+
>
|
|
1006
|
+
<span class="tool-chip-name">{{
|
|
1007
|
+
toolChipLabel(tool)
|
|
1008
|
+
}}</span>
|
|
1009
|
+
<span class="tool-chip-status">{{
|
|
1010
|
+
toolChipStatus(tool, message)
|
|
1011
|
+
}}</span>
|
|
1012
|
+
<span
|
|
1013
|
+
v-if="
|
|
1014
|
+
expandedTools.has(tool.id) && tool.text !== undefined
|
|
1015
|
+
"
|
|
1016
|
+
class="tool-chip-result"
|
|
1017
|
+
>{{ tool.text }}</span
|
|
1018
|
+
>
|
|
1019
|
+
</button>
|
|
1020
|
+
</div>
|
|
1021
|
+
<!--
|
|
910
1022
|
The subagents this Turn dispatched. The child's own Session is
|
|
911
1023
|
never in this transcript, so the chip is the whole of what the
|
|
912
1024
|
conversation says about it; opening one shows the summary the
|
|
913
1025
|
child handed back and nothing else.
|
|
914
1026
|
-->
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
:key="entry.chip.taskId"
|
|
919
|
-
type="button"
|
|
920
|
-
class="task-chip"
|
|
921
|
-
:class="`task-chip-${entry.status}`"
|
|
922
|
-
:aria-expanded="expandedTasks.has(entry.chip.taskId)"
|
|
923
|
-
@click="toggleTask(entry.chip.taskId)"
|
|
1027
|
+
<div
|
|
1028
|
+
v-if="taskChipsOf(message).length > 0"
|
|
1029
|
+
class="message-tasks"
|
|
924
1030
|
>
|
|
925
|
-
<
|
|
926
|
-
|
|
927
|
-
entry.chip.
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
class="task-chip-summary"
|
|
934
|
-
>{{
|
|
935
|
-
entry.summary ??
|
|
936
|
-
"This subagent has not reported a summary yet."
|
|
937
|
-
}}</span
|
|
1031
|
+
<button
|
|
1032
|
+
v-for="entry in taskChipsOf(message)"
|
|
1033
|
+
:key="entry.chip.taskId"
|
|
1034
|
+
type="button"
|
|
1035
|
+
class="task-chip"
|
|
1036
|
+
:class="`task-chip-${entry.status}`"
|
|
1037
|
+
:aria-expanded="expandedTasks.has(entry.chip.taskId)"
|
|
1038
|
+
@click="toggleTask(entry.chip.taskId)"
|
|
938
1039
|
>
|
|
939
|
-
|
|
1040
|
+
<span class="task-chip-type">{{
|
|
1041
|
+
entry.chip.taskType
|
|
1042
|
+
}}</span>
|
|
1043
|
+
<span class="task-chip-description">{{
|
|
1044
|
+
entry.chip.description
|
|
1045
|
+
}}</span>
|
|
1046
|
+
<span class="task-chip-status">{{ entry.status }}</span>
|
|
1047
|
+
<span class="task-chip-model">{{ entry.chip.model }}</span>
|
|
1048
|
+
<span
|
|
1049
|
+
v-if="expandedTasks.has(entry.chip.taskId)"
|
|
1050
|
+
class="task-chip-summary"
|
|
1051
|
+
>{{
|
|
1052
|
+
entry.summary ??
|
|
1053
|
+
"This subagent has not reported a summary yet."
|
|
1054
|
+
}}</span
|
|
1055
|
+
>
|
|
1056
|
+
<!--
|
|
940
1057
|
Cancellation is the User's, explicit and authenticated. It
|
|
941
1058
|
is offered only while the subagent is live: a settled one
|
|
942
1059
|
has an outcome, and stopping it would rewrite it.
|
|
943
1060
|
-->
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1061
|
+
<span
|
|
1062
|
+
v-if="
|
|
1063
|
+
expandedTasks.has(entry.chip.taskId) &&
|
|
1064
|
+
isTaskLive(entry.status)
|
|
1065
|
+
"
|
|
1066
|
+
class="task-chip-stop"
|
|
1067
|
+
role="button"
|
|
1068
|
+
tabindex="0"
|
|
1069
|
+
@click.stop="stopTask(entry.chip.taskId)"
|
|
1070
|
+
@keydown.enter.stop.prevent="stopTask(entry.chip.taskId)"
|
|
1071
|
+
@keydown.space.stop.prevent="stopTask(entry.chip.taskId)"
|
|
1072
|
+
>Stop this subagent</span
|
|
1073
|
+
>
|
|
1074
|
+
</button>
|
|
1075
|
+
</div>
|
|
958
1076
|
</div>
|
|
959
1077
|
</template>
|
|
960
1078
|
<div v-else class="message-bubble">{{ message.text }}</div>
|
|
@@ -985,7 +1103,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
985
1103
|
type="button"
|
|
986
1104
|
@click="web.resumeRun(state.activeRun.runId)"
|
|
987
1105
|
>
|
|
988
|
-
|
|
1106
|
+
Try again
|
|
989
1107
|
</button>
|
|
990
1108
|
</div>
|
|
991
1109
|
</Transition>
|
|
@@ -34,6 +34,13 @@ const providedWeb = inject(frockBotWebDataKey);
|
|
|
34
34
|
if (!providedWeb) throw new Error("Package iframe host data was not provided");
|
|
35
35
|
const web = providedWeb;
|
|
36
36
|
const frame = ref<HTMLIFrameElement>();
|
|
37
|
+
/** Where a page came from, said plainly. First-party pages say nothing. */
|
|
38
|
+
const provenanceLabel = computed(() => {
|
|
39
|
+
if (props.contribution.provenance === "Bot-authored")
|
|
40
|
+
return "Built by this Bot";
|
|
41
|
+
if (props.contribution.provenance === "User-installed") return "Added by you";
|
|
42
|
+
return undefined;
|
|
43
|
+
});
|
|
37
44
|
const height = ref(240);
|
|
38
45
|
const failure = ref<string>();
|
|
39
46
|
const lastStateWireByName = new Map<string, string>();
|
|
@@ -123,7 +130,9 @@ function post(message: PackageIframeHostMessageV2): void {
|
|
|
123
130
|
postPackageIframeHostMessage(target, message, lastStateWireByName);
|
|
124
131
|
} catch (error) {
|
|
125
132
|
failure.value =
|
|
126
|
-
error instanceof Error
|
|
133
|
+
error instanceof Error
|
|
134
|
+
? error.message
|
|
135
|
+
: "This page sent something FrockBot couldn't read.";
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
|
|
@@ -179,7 +188,7 @@ async function onMessage(event: MessageEvent): Promise<void> {
|
|
|
179
188
|
}
|
|
180
189
|
if (message.type === "focus") {
|
|
181
190
|
if (!packageIframeFocusAllowedV2(props.contribution)) {
|
|
182
|
-
failure.value = "This
|
|
191
|
+
failure.value = "This plugin can't change which Applet is open.";
|
|
183
192
|
return;
|
|
184
193
|
}
|
|
185
194
|
failure.value = undefined;
|
|
@@ -189,7 +198,7 @@ async function onMessage(event: MessageEvent): Promise<void> {
|
|
|
189
198
|
if (message.type === "openExternal") {
|
|
190
199
|
const origin = catalog.value?.artifactOrigin;
|
|
191
200
|
if (!origin || !packageIframeExternalUrlAllowedV2(message.url, origin)) {
|
|
192
|
-
failure.value = "This
|
|
201
|
+
failure.value = "This plugin can only open its own pages.";
|
|
193
202
|
return;
|
|
194
203
|
}
|
|
195
204
|
failure.value = undefined;
|
|
@@ -197,7 +206,7 @@ async function onMessage(event: MessageEvent): Promise<void> {
|
|
|
197
206
|
return;
|
|
198
207
|
}
|
|
199
208
|
if (!packageIframeToolAllowedV1(props.contribution, message.name)) {
|
|
200
|
-
failure.value = `This
|
|
209
|
+
failure.value = `This plugin isn't allowed to use ${message.name}.`;
|
|
201
210
|
return;
|
|
202
211
|
}
|
|
203
212
|
failure.value = undefined;
|
|
@@ -232,12 +241,12 @@ onBeforeUnmount(() => window.removeEventListener("message", onMessage));
|
|
|
232
241
|
<section class="package-iframe-frame" :class="`package-iframe-${layout}`">
|
|
233
242
|
<header v-if="attribution" class="package-iframe-attribution">
|
|
234
243
|
<strong>{{ contribution.displayName }}</strong>
|
|
235
|
-
<span>{{
|
|
244
|
+
<span v-if="provenanceLabel">{{ provenanceLabel }}</span>
|
|
236
245
|
</header>
|
|
237
246
|
<!-- 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. -->
|
|
238
247
|
<iframe
|
|
239
248
|
ref="frame"
|
|
240
|
-
:title="
|
|
249
|
+
:title="contribution.displayName"
|
|
241
250
|
:src="source"
|
|
242
251
|
:style="layout === 'fill' ? undefined : { height: `${height}px` }"
|
|
243
252
|
sandbox="allow-scripts"
|