@frockbot/plugin-shell 0.3.5 → 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 +13 -14
- package/src/backend.ts +28 -4
- package/src/client/AppletCanvas.vue +3 -5
- package/src/client/FrockBotApp.vue +163 -106
- package/src/client/PackageIframeHost.vue +15 -6
- package/src/client/index.test.ts +101 -12
- package/src/client/index.ts +113 -45
- package/src/client/model-presentation.test.ts +4 -2
- package/src/client/model-presentation.ts +2 -2
- package/src/client/styles.css +15 -1
- package/src/run-protocol.ts +15 -3
package/src/client/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { decodeStartConnectionResultV1 } from "@frockbot/connection-core";
|
|
|
55
55
|
import { decodeClientSkillCatalogV1 } from "../skill-protocol.js";
|
|
56
56
|
import {
|
|
57
57
|
ClientTurnRefusedErrorV1,
|
|
58
|
+
type ClientTurnRefusalReasonV1,
|
|
58
59
|
decodeClientTurnV1,
|
|
59
60
|
} from "../run-protocol.js";
|
|
60
61
|
import {
|
|
@@ -243,10 +244,8 @@ function activeRunView(run: ClientRun): WebActiveRun | undefined {
|
|
|
243
244
|
runId: run.runId,
|
|
244
245
|
status: run.status,
|
|
245
246
|
message: run.stopRequestedAt
|
|
246
|
-
? "
|
|
247
|
-
:
|
|
248
|
-
run.failure ??
|
|
249
|
-
"This Turn requires provider reconciliation before it can continue."),
|
|
247
|
+
? "Stopping…"
|
|
248
|
+
: "Something went wrong mid-reply. Try again to pick it up.",
|
|
250
249
|
// Offered whenever the run is parked, Stop included. Hiding it there
|
|
251
250
|
// hid it in exactly the case Stop creates: a Turn that was stopped
|
|
252
251
|
// while the model was mid-answer parks, and the person was left with a
|
|
@@ -257,6 +256,31 @@ function activeRunView(run: ClientRun): WebActiveRun | undefined {
|
|
|
257
256
|
return undefined;
|
|
258
257
|
}
|
|
259
258
|
|
|
259
|
+
/**
|
|
260
|
+
* What a refused send tells the person. The refusal's own `error` names the
|
|
261
|
+
* durable invariant that declined it, which the debug surface needs and the
|
|
262
|
+
* composer does not, so the typed reason picks the sentence instead.
|
|
263
|
+
*/
|
|
264
|
+
function turnRefusalCopyV1(reason: ClientTurnRefusalReasonV1): string {
|
|
265
|
+
if (reason === "busy")
|
|
266
|
+
return "This Bot is still working on your last message.";
|
|
267
|
+
if (reason === "reconciliation-required")
|
|
268
|
+
return "This Bot's last reply stopped partway. Try again to continue it.";
|
|
269
|
+
if (reason === "duplicate") return "That message was already sent.";
|
|
270
|
+
return "That message didn't go through. Try sending it again.";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The Bot's voice is its sends. When a Turn delivered anything to the User the
|
|
275
|
+
* model's own assistant text is scratch space and the thread does not draw it
|
|
276
|
+
* (issue 153): drawing both is how a one-word reply arrived twice, once as the
|
|
277
|
+
* model's text and once as the bubble that was actually delivered.
|
|
278
|
+
*/
|
|
279
|
+
function visibleAssistantText(run: ClientRun, fallback = ""): string {
|
|
280
|
+
if (sendsFrom(run.events).length > 0) return "";
|
|
281
|
+
return run.responseText ?? fallback;
|
|
282
|
+
}
|
|
283
|
+
|
|
260
284
|
function isTerminalRun(run: ClientRun): boolean {
|
|
261
285
|
return (
|
|
262
286
|
run.status === "completed" ||
|
|
@@ -277,7 +301,7 @@ function assistantMessage(
|
|
|
277
301
|
id: `${run.runId}:assistant`,
|
|
278
302
|
runId: run.runId,
|
|
279
303
|
role: "assistant",
|
|
280
|
-
text: run
|
|
304
|
+
text: visibleAssistantText(run),
|
|
281
305
|
status: "streaming",
|
|
282
306
|
// A Turn that has not started shows nothing of its own: the greyed user
|
|
283
307
|
// message is the whole of what the thread says about it.
|
|
@@ -294,8 +318,8 @@ function assistantMessage(
|
|
|
294
318
|
id: `${run.runId}:assistant`,
|
|
295
319
|
runId: run.runId,
|
|
296
320
|
role: "assistant",
|
|
297
|
-
text: run
|
|
298
|
-
notice:
|
|
321
|
+
text: visibleAssistantText(run),
|
|
322
|
+
notice: "Interrupted by your next message.",
|
|
299
323
|
status: "aborted",
|
|
300
324
|
tools: toolsFrom(run.events),
|
|
301
325
|
sends: sendsFrom(run.events),
|
|
@@ -307,10 +331,7 @@ function assistantMessage(
|
|
|
307
331
|
id: `${run.runId}:assistant`,
|
|
308
332
|
runId: run.runId,
|
|
309
333
|
role: "assistant",
|
|
310
|
-
text:
|
|
311
|
-
run.recovery?.message ??
|
|
312
|
-
run.failure ??
|
|
313
|
-
"Provider reconciliation is required before this Turn can continue.",
|
|
334
|
+
text: "This reply stopped partway. Try again to continue it.",
|
|
314
335
|
status: "reconciliation-required",
|
|
315
336
|
tools: toolsFrom(run.events),
|
|
316
337
|
sends: sendsFrom(run.events),
|
|
@@ -322,22 +343,39 @@ function assistantMessage(
|
|
|
322
343
|
id: `${run.runId}:assistant`,
|
|
323
344
|
runId: run.runId,
|
|
324
345
|
role: "assistant",
|
|
325
|
-
text: run
|
|
326
|
-
notice:
|
|
346
|
+
text: visibleAssistantText(run),
|
|
347
|
+
notice: "You stopped this.",
|
|
327
348
|
status: "aborted",
|
|
328
349
|
tools: toolsFrom(run.events),
|
|
329
350
|
sends: sendsFrom(run.events),
|
|
330
351
|
tasks: tasksFrom(run.events),
|
|
331
352
|
};
|
|
332
353
|
}
|
|
354
|
+
// A Turn that broke after it had started talking keeps what it said, with
|
|
355
|
+
// the reason underneath it — the treatment a stopped Turn already gets, for
|
|
356
|
+
// the same reason: the words arrived and the person read them (ADR 0028).
|
|
357
|
+
// A Turn that broke before saying anything is still just the reason.
|
|
358
|
+
if (run.status === "failed" && run.responseText) {
|
|
359
|
+
return {
|
|
360
|
+
id: `${run.runId}:assistant`,
|
|
361
|
+
runId: run.runId,
|
|
362
|
+
role: "assistant",
|
|
363
|
+
text: run.responseText,
|
|
364
|
+
notice: run.failure ?? "Agent request failed.",
|
|
365
|
+
status: "error",
|
|
366
|
+
tools: toolsFrom(run.events),
|
|
367
|
+
sends: sendsFrom(run.events),
|
|
368
|
+
tasks: tasksFrom(run.events),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
333
371
|
return {
|
|
334
372
|
id: `${run.runId}:assistant`,
|
|
335
373
|
runId: run.runId,
|
|
336
374
|
role: "assistant",
|
|
337
375
|
text:
|
|
338
376
|
run.status === "failed"
|
|
339
|
-
?
|
|
340
|
-
: (run
|
|
377
|
+
? "This Bot couldn't finish its reply. Try again."
|
|
378
|
+
: visibleAssistantText(run, notification?.body ?? ""),
|
|
341
379
|
status: run.status === "failed" ? "error" : "completed",
|
|
342
380
|
tools: toolsFrom(run.events),
|
|
343
381
|
sends: sendsFrom(run.events),
|
|
@@ -429,8 +467,12 @@ export function projectDurableRuns(
|
|
|
429
467
|
activeRun = activeRunView(run) ?? activeRun;
|
|
430
468
|
if (run.status === "running" || run.status === "reconciliation-required") {
|
|
431
469
|
busyRunId = run.runId;
|
|
432
|
-
if (!run.queued) runningRunId = run.runId;
|
|
433
470
|
}
|
|
471
|
+
// Stop belongs to a Turn that is executing. A Turn parked on a
|
|
472
|
+
// reconciliation is busy but not running: there is nothing to stop, and
|
|
473
|
+
// offering it left a Stop button standing for good — across reloads,
|
|
474
|
+
// because the state it was keyed off never became terminal.
|
|
475
|
+
if (run.status === "running" && !run.queued) runningRunId = run.runId;
|
|
434
476
|
if (notification && isTerminalRun(run)) {
|
|
435
477
|
projected.add(notification.notificationId);
|
|
436
478
|
}
|
|
@@ -448,7 +490,13 @@ export function projectDurableRuns(
|
|
|
448
490
|
state.activeRunId = undefined;
|
|
449
491
|
}
|
|
450
492
|
if (runningRunId) state.runningRunId = runningRunId;
|
|
451
|
-
else if (
|
|
493
|
+
else if (
|
|
494
|
+
state.runningRunId &&
|
|
495
|
+
runs.some((run) => run.runId === state.runningRunId)
|
|
496
|
+
) {
|
|
497
|
+
// The channel is carrying this run and it is not executing, whatever it
|
|
498
|
+
// settled as. A run the list does not carry yet is the one this tab just
|
|
499
|
+
// submitted, which keeps its Stop.
|
|
452
500
|
state.runningRunId = undefined;
|
|
453
501
|
}
|
|
454
502
|
if (activeRun) state.activeRun = activeRun;
|
|
@@ -704,7 +752,7 @@ export function decodePluginCatalog(value: unknown): PluginCatalogItem[] {
|
|
|
704
752
|
!Array.isArray(value.packages) ||
|
|
705
753
|
value.packages.length > 256
|
|
706
754
|
) {
|
|
707
|
-
throw new Error("
|
|
755
|
+
throw new Error("FrockBot couldn't load this deployment. Reload the page.");
|
|
708
756
|
}
|
|
709
757
|
return value.packages.flatMap((candidate) => {
|
|
710
758
|
if (
|
|
@@ -737,7 +785,9 @@ export function decodePluginCatalog(value: unknown): PluginCatalogItem[] {
|
|
|
737
785
|
kind === "mobile",
|
|
738
786
|
)
|
|
739
787
|
) {
|
|
740
|
-
throw new Error(
|
|
788
|
+
throw new Error(
|
|
789
|
+
"FrockBot couldn't load this deployment. Reload the page.",
|
|
790
|
+
);
|
|
741
791
|
}
|
|
742
792
|
const decoded = decodeFrockBotManifest({
|
|
743
793
|
// v4, so a Capability carrying an admission ceiling decodes here too.
|
|
@@ -919,7 +969,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
919
969
|
web.value.activeRun = {
|
|
920
970
|
runId,
|
|
921
971
|
status: "running",
|
|
922
|
-
message: "
|
|
972
|
+
message: "Checking whether your message went through…",
|
|
923
973
|
canResume: false,
|
|
924
974
|
};
|
|
925
975
|
if (!ctx.transport.lookupRun || !ctx.transport.fenceRunAdmission) {
|
|
@@ -959,7 +1009,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
959
1009
|
reconciliationError = `${
|
|
960
1010
|
error instanceof Error
|
|
961
1011
|
? error.message
|
|
962
|
-
: "
|
|
1012
|
+
: "Couldn't check on your message."
|
|
963
1013
|
} Retrying…`;
|
|
964
1014
|
web.value.settingsError = reconciliationError;
|
|
965
1015
|
}
|
|
@@ -991,7 +1041,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
991
1041
|
) {
|
|
992
1042
|
return;
|
|
993
1043
|
}
|
|
994
|
-
if (!run) throw new Error("
|
|
1044
|
+
if (!run) throw new Error("Couldn't load that reply.");
|
|
995
1045
|
if (web.value.settingsError === observationError) {
|
|
996
1046
|
web.value.settingsError = undefined;
|
|
997
1047
|
}
|
|
@@ -1001,7 +1051,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1001
1051
|
} catch (error) {
|
|
1002
1052
|
if (signal.aborted) return;
|
|
1003
1053
|
observationError = `${
|
|
1004
|
-
error instanceof Error ? error.message : "
|
|
1054
|
+
error instanceof Error ? error.message : "Couldn't load that reply."
|
|
1005
1055
|
} Retrying…`;
|
|
1006
1056
|
web.value.settingsError = observationError;
|
|
1007
1057
|
}
|
|
@@ -1189,7 +1239,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1189
1239
|
|
|
1190
1240
|
const web: Ref<ShellWebData> = ref({
|
|
1191
1241
|
connection: "ready",
|
|
1192
|
-
modelLabel: "
|
|
1242
|
+
modelLabel: "No model available — set one up in Models",
|
|
1193
1243
|
modelReady: false,
|
|
1194
1244
|
modelSource: "none",
|
|
1195
1245
|
settingsAvailable: true,
|
|
@@ -1519,12 +1569,10 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1519
1569
|
const botId = web.value.activeBotId;
|
|
1520
1570
|
const catalog = web.value.packageUi;
|
|
1521
1571
|
if (!post || !botId || !catalog || catalog.botId !== botId) {
|
|
1522
|
-
throw new Error("
|
|
1572
|
+
throw new Error("That plugin's page isn't available.");
|
|
1523
1573
|
}
|
|
1524
1574
|
if (!packageIframeToolAllowedV1(contribution, name)) {
|
|
1525
|
-
throw new Error(
|
|
1526
|
-
`Package "${contribution.packageId}" did not declare tool "${name}"`,
|
|
1527
|
-
);
|
|
1575
|
+
throw new Error(`That plugin isn't allowed to use ${name}.`);
|
|
1528
1576
|
}
|
|
1529
1577
|
const turn = decodeClientTurnV1(
|
|
1530
1578
|
await post(
|
|
@@ -1951,9 +1999,22 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1951
1999
|
updateSettingsLoadError("package-catalog");
|
|
1952
2000
|
} catch (error) {
|
|
1953
2001
|
if (generation !== packageCatalogGeneration) return;
|
|
2002
|
+
// The gateway answers 404 `catalog generation was not found` when the
|
|
2003
|
+
// deployment has published no Catalog at all. That is a state, not a
|
|
2004
|
+
// fault, and the raw server sentence means nothing to a person — so it
|
|
2005
|
+
// is translated here and the surface renders it instead of the
|
|
2006
|
+
// "nothing matched your search" empty state.
|
|
2007
|
+
const raw =
|
|
2008
|
+
error instanceof Error ? error.message : "Could not load the Catalog";
|
|
2009
|
+
web.value.packageCatalog = [];
|
|
2010
|
+
web.value.packageCatalogGeneration = undefined;
|
|
1954
2011
|
updateSettingsLoadError(
|
|
1955
2012
|
"package-catalog",
|
|
1956
|
-
|
|
2013
|
+
/catalog generation was not found|Package Catalog is not configured/.test(
|
|
2014
|
+
raw,
|
|
2015
|
+
)
|
|
2016
|
+
? "No plugins are published for this deployment yet."
|
|
2017
|
+
: `Plugins could not be loaded: ${raw}`,
|
|
1957
2018
|
);
|
|
1958
2019
|
}
|
|
1959
2020
|
},
|
|
@@ -1983,7 +2044,8 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
1983
2044
|
if (!settings || !ctx.transport.executeConfiguration) {
|
|
1984
2045
|
throw new Error("Plugins are unavailable");
|
|
1985
2046
|
}
|
|
1986
|
-
if (!generation)
|
|
2047
|
+
if (!generation)
|
|
2048
|
+
throw new Error("The catalog isn't loaded yet. Try again in a moment.");
|
|
1987
2049
|
const receipt = await ctx.transport.executeConfiguration({
|
|
1988
2050
|
schemaVersion: 1,
|
|
1989
2051
|
type: "user/install-package",
|
|
@@ -2266,8 +2328,8 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2266
2328
|
if (result.status !== "applied") {
|
|
2267
2329
|
throw new Error(
|
|
2268
2330
|
result.status === "reconciliation-required"
|
|
2269
|
-
? "
|
|
2270
|
-
: "
|
|
2331
|
+
? "Disconnecting didn't finish. Try again."
|
|
2332
|
+
: "Couldn't disconnect that account.",
|
|
2271
2333
|
);
|
|
2272
2334
|
}
|
|
2273
2335
|
},
|
|
@@ -2370,7 +2432,10 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2370
2432
|
id: `${result.runId}:assistant`,
|
|
2371
2433
|
runId: result.runId,
|
|
2372
2434
|
role: "assistant",
|
|
2373
|
-
|
|
2435
|
+
// The same rule the durable projection follows: a Turn that
|
|
2436
|
+
// delivered something speaks through its sends, not through the
|
|
2437
|
+
// model's own text (issue 153).
|
|
2438
|
+
text: sendsFrom(result.events).length > 0 ? "" : result.text,
|
|
2374
2439
|
at: optimisticAt,
|
|
2375
2440
|
status: "completed",
|
|
2376
2441
|
tools: toolsFrom(result.events),
|
|
@@ -2401,8 +2466,9 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2401
2466
|
// that was never admitted only threw the reason away.
|
|
2402
2467
|
if (error instanceof ClientTurnRefusedErrorV1) {
|
|
2403
2468
|
removeMessages(web.value.messages, pendingRunId);
|
|
2404
|
-
|
|
2405
|
-
|
|
2469
|
+
const refusal = turnRefusalCopyV1(error.refusal.reason);
|
|
2470
|
+
web.value.error = refusal;
|
|
2471
|
+
return { accepted: false, error: refusal };
|
|
2406
2472
|
}
|
|
2407
2473
|
const aborted =
|
|
2408
2474
|
error instanceof DOMException && error.name === "AbortError";
|
|
@@ -2410,9 +2476,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2410
2476
|
id: `${pendingRunId}:assistant`,
|
|
2411
2477
|
runId: pendingRunId,
|
|
2412
2478
|
role: "assistant",
|
|
2413
|
-
text:
|
|
2414
|
-
? "Request stopped locally; checking whether it started."
|
|
2415
|
-
: "Confirming whether this Turn was admitted.",
|
|
2479
|
+
text: "Checking whether your message went through…",
|
|
2416
2480
|
at: optimisticAt,
|
|
2417
2481
|
status: "interrupted",
|
|
2418
2482
|
tools: [],
|
|
@@ -2441,14 +2505,18 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2441
2505
|
id: `${pendingRunId}:assistant`,
|
|
2442
2506
|
runId: pendingRunId,
|
|
2443
2507
|
role: "assistant",
|
|
2444
|
-
text: "
|
|
2508
|
+
text: "Your message didn't go through. Try sending it again.",
|
|
2445
2509
|
at: optimisticAt,
|
|
2446
2510
|
status: "error",
|
|
2447
2511
|
tools: [],
|
|
2448
2512
|
sends: [],
|
|
2449
2513
|
});
|
|
2450
|
-
web.value.error =
|
|
2451
|
-
|
|
2514
|
+
web.value.error =
|
|
2515
|
+
"Your message didn't go through. Try sending it again.";
|
|
2516
|
+
return {
|
|
2517
|
+
accepted: false,
|
|
2518
|
+
error: "Your message didn't go through. Try sending it again.",
|
|
2519
|
+
};
|
|
2452
2520
|
}
|
|
2453
2521
|
return { accepted: true, runId: pendingRunId };
|
|
2454
2522
|
} finally {
|
|
@@ -2471,14 +2539,14 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2471
2539
|
},
|
|
2472
2540
|
async resumeRun(runId: string): Promise<void> {
|
|
2473
2541
|
if (!ctx.transport.reconcileRun) {
|
|
2474
|
-
web.value.settingsError = "
|
|
2542
|
+
web.value.settingsError = "Can't retry this right now.";
|
|
2475
2543
|
return;
|
|
2476
2544
|
}
|
|
2477
2545
|
if (web.value.activeRun?.runId !== runId) return;
|
|
2478
2546
|
web.value.activeRun = {
|
|
2479
2547
|
runId,
|
|
2480
2548
|
status: "running",
|
|
2481
|
-
message: "
|
|
2549
|
+
message: "Retrying…",
|
|
2482
2550
|
canResume: false,
|
|
2483
2551
|
};
|
|
2484
2552
|
const botId = web.value.activeBotId;
|
|
@@ -2487,7 +2555,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2487
2555
|
await ctx.transport.reconcileRun(botId, runId);
|
|
2488
2556
|
} catch (error) {
|
|
2489
2557
|
web.value.settingsError =
|
|
2490
|
-
error instanceof Error ? error.message : "
|
|
2558
|
+
error instanceof Error ? error.message : "Couldn't retry that.";
|
|
2491
2559
|
}
|
|
2492
2560
|
try {
|
|
2493
2561
|
await deliverNotifications(botId);
|
|
@@ -2495,7 +2563,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
|
|
|
2495
2563
|
web.value.settingsError =
|
|
2496
2564
|
error instanceof Error
|
|
2497
2565
|
? error.message
|
|
2498
|
-
: "
|
|
2566
|
+
: "Couldn't refresh this reply.";
|
|
2499
2567
|
}
|
|
2500
2568
|
},
|
|
2501
2569
|
async stopRun(): Promise<void> {
|
|
@@ -31,12 +31,14 @@ describe("model runtime presentation", () => {
|
|
|
31
31
|
"Llama 3 · Ollama Cloud · Account model",
|
|
32
32
|
);
|
|
33
33
|
expect(modelRuntimeLabel({ ...label, source: "bot" })).toBe(
|
|
34
|
-
"Llama 3 · Ollama Cloud · Bot
|
|
34
|
+
"Llama 3 · Ollama Cloud · this Bot only",
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
test("shows unavailable and backend failure states", () => {
|
|
39
|
-
expect(modelRuntimeLabel({ source: "none" })).toBe(
|
|
39
|
+
expect(modelRuntimeLabel({ source: "none" })).toBe(
|
|
40
|
+
"No model available — set one up in Models",
|
|
41
|
+
);
|
|
40
42
|
expect(
|
|
41
43
|
modelRuntimeLabel({
|
|
42
44
|
source: "account",
|
|
@@ -14,13 +14,13 @@ export function modelRuntimeLabel(input: {
|
|
|
14
14
|
}): string {
|
|
15
15
|
if (input.failure) return input.failure;
|
|
16
16
|
if (input.source === "none" || !input.providerModelId) {
|
|
17
|
-
return "
|
|
17
|
+
return "No model available — set one up in Models";
|
|
18
18
|
}
|
|
19
19
|
const model =
|
|
20
20
|
input.modelDisplayName ?? input.providerModelId ?? "Connected model";
|
|
21
21
|
const provider = input.packageDisplayName ?? input.connectionDisplayName;
|
|
22
22
|
const runtime = provider ? `${model} · ${provider}` : model;
|
|
23
|
-
if (input.source === "bot") return `${runtime} · Bot
|
|
23
|
+
if (input.source === "bot") return `${runtime} · this Bot only`;
|
|
24
24
|
if (input.source === "account") return `${runtime} · Account model`;
|
|
25
25
|
return runtime;
|
|
26
26
|
}
|
package/src/client/styles.css
CHANGED
|
@@ -301,13 +301,27 @@
|
|
|
301
301
|
font-size: var(--frock-text-xs);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
/*
|
|
304
|
+
/*
|
|
305
|
+
* An assistant Turn is its avatar and, beside it, one column holding
|
|
306
|
+
* everything the Turn produced. The row has exactly two children: bubbles,
|
|
307
|
+
* notices, sends and chips stack inside the column, so a one-word reply is a
|
|
308
|
+
* bubble the width of its word rather than a sliver of a shared row.
|
|
309
|
+
*/
|
|
305
310
|
.message-assistant {
|
|
306
311
|
flex-direction: row;
|
|
307
312
|
align-items: flex-start;
|
|
308
313
|
gap: 8px;
|
|
309
314
|
}
|
|
310
315
|
|
|
316
|
+
.message-column {
|
|
317
|
+
display: flex;
|
|
318
|
+
min-width: 0;
|
|
319
|
+
flex: 1 1 auto;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
align-items: flex-start;
|
|
322
|
+
gap: 6px;
|
|
323
|
+
}
|
|
324
|
+
|
|
311
325
|
.bot-avatar {
|
|
312
326
|
position: relative;
|
|
313
327
|
display: grid;
|
package/src/run-protocol.ts
CHANGED
|
@@ -179,7 +179,12 @@ export interface ClientDynamicToolCallInputV1 {
|
|
|
179
179
|
|
|
180
180
|
export type ClientRunOutcomeV1 =
|
|
181
181
|
| { type: "completed"; text: string }
|
|
182
|
-
|
|
182
|
+
/**
|
|
183
|
+
* A Turn that broke keeps what it had already said, for the same reason a
|
|
184
|
+
* stopped one does: the words arrived, the person read them, and replacing
|
|
185
|
+
* them with a notice would rewrite what they watched happen (ADR 0028).
|
|
186
|
+
*/
|
|
187
|
+
| { type: "failed"; message: string; text?: string }
|
|
183
188
|
/**
|
|
184
189
|
* A Turn a Stop or a later message ended keeps what it had already said:
|
|
185
190
|
* `text` is that partial answer, and `message` is the line saying why it
|
|
@@ -673,6 +678,7 @@ export function projectClientRunV1(run: StoredRun): ClientRunV1 {
|
|
|
673
678
|
run.failure ?? "Agent request failed.",
|
|
674
679
|
MAX_FAILURE_BYTES,
|
|
675
680
|
),
|
|
681
|
+
...interruptedOutcomeTextV1(run),
|
|
676
682
|
} satisfies ClientRunOutcomeV1)
|
|
677
683
|
: status === "cancelled"
|
|
678
684
|
? ({
|
|
@@ -1121,10 +1127,11 @@ function decodeOutcome(
|
|
|
1121
1127
|
};
|
|
1122
1128
|
}
|
|
1123
1129
|
if (outcome.type === "failed" && runStatus === "failed") {
|
|
1124
|
-
exactKeys(outcome, ["type", "message"], "run.outcome");
|
|
1130
|
+
exactKeys(outcome, ["type", "message", "text"], "run.outcome");
|
|
1125
1131
|
return {
|
|
1126
1132
|
type: "failed",
|
|
1127
1133
|
message: wireString(outcome, "message", MAX_FAILURE_BYTES, "run.outcome"),
|
|
1134
|
+
...decodeInterruptedTextV1(outcome),
|
|
1128
1135
|
};
|
|
1129
1136
|
}
|
|
1130
1137
|
if (outcome.type === "cancelled" && runStatus === "cancelled") {
|
|
@@ -1249,7 +1256,12 @@ function decodeRun(value: unknown): ClientRun {
|
|
|
1249
1256
|
...(stopRequestedAt ? { stopRequestedAt } : {}),
|
|
1250
1257
|
...(run.queued === true ? { queued: true as const } : {}),
|
|
1251
1258
|
...(outcome?.type === "completed" ? { responseText: outcome.text } : {}),
|
|
1252
|
-
...(outcome?.type === "failed"
|
|
1259
|
+
...(outcome?.type === "failed"
|
|
1260
|
+
? {
|
|
1261
|
+
failure: outcome.message,
|
|
1262
|
+
...(outcome.text ? { responseText: outcome.text } : {}),
|
|
1263
|
+
}
|
|
1264
|
+
: {}),
|
|
1253
1265
|
...(outcome?.type === "cancelled" || outcome?.type === "superseded"
|
|
1254
1266
|
? {
|
|
1255
1267
|
failure: outcome.message,
|