@frockbot/plugin-shell 0.3.1 → 0.3.3
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 -29
- package/src/agent.test.ts +15 -3
- package/src/backend-applets.test.ts +581 -0
- package/src/backend-applets.ts +959 -0
- package/src/backend-authoring.test.ts +61 -19
- package/src/backend-authoring.ts +66 -27
- package/src/backend-completion.ts +4 -2
- package/src/backend-composition.ts +64 -0
- package/src/backend-computer.test.ts +128 -0
- package/src/backend-computer.ts +81 -0
- package/src/backend-configuration.test.ts +8 -1
- package/src/backend-iframe-ui.test.ts +29 -12
- package/src/backend-isolate.ts +31 -5
- package/src/backend-package-catalog.test.ts +13 -8
- package/src/backend-package-catalog.ts +8 -6
- package/src/backend-recovery-integration.test.ts +23 -0
- package/src/backend-recovery.ts +20 -12
- package/src/backend-runner-iframe.test.ts +10 -1
- package/src/backend-runner.ts +9 -2
- package/src/backend-stop.test.ts +6 -6
- package/src/backend-supersede.test.ts +377 -0
- package/src/backend.ts +567 -13
- package/src/client/AppletCanvas.vue +679 -0
- package/src/client/FrockBotApp.vue +195 -21
- package/src/client/PackageEntryTrigger.vue +77 -0
- package/src/client/PackageIframeHost.vue +148 -47
- package/src/client/PackageIframeSettings.vue +8 -6
- package/src/client/PackageSurfacePage.vue +39 -0
- package/src/client/applets-client.test.ts +204 -0
- package/src/client/applets-client.ts +139 -0
- package/src/client/applets-state.ts +64 -0
- package/src/client/index.test.ts +221 -7
- package/src/client/index.ts +398 -6
- package/src/client/package-iframe-entries.test.ts +122 -0
- package/src/client/package-iframe-entries.ts +112 -0
- package/src/client/package-iframe-host-message.test.ts +3 -3
- package/src/client/package-iframe-host-message.ts +3 -3
- package/src/client/styles.css +118 -1
- package/src/composition-views.ts +31 -6
- package/src/run-protocol.test.ts +92 -0
- package/src/run-protocol.ts +193 -17
- package/src/shared.ts +70 -0
- package/src/terminal-records.test.ts +52 -1
- package/src/terminal-records.ts +48 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client half of the Applet routes.
|
|
3
|
+
*
|
|
4
|
+
* The backend is the authority for every one of these reads; this module is
|
|
5
|
+
* only the typed seam between the hosted transport and the shell's projection,
|
|
6
|
+
* so a route that is absent from a deployment reads as "no Applets" rather
|
|
7
|
+
* than as an error over the User's conversation. Every response crosses a
|
|
8
|
+
* strict decoder before it reaches the shell.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
decodeAppletBuildViewV1,
|
|
12
|
+
decodeAppletFocusViewV1,
|
|
13
|
+
decodeAppletListViewV1,
|
|
14
|
+
decodeAppletSourceViewV1,
|
|
15
|
+
decodeAppletUiViewV1,
|
|
16
|
+
decodeAppletViewerTokenV1,
|
|
17
|
+
type AppletBuildViewV1,
|
|
18
|
+
type AppletSourceViewV1,
|
|
19
|
+
type AppletSummaryV1,
|
|
20
|
+
type AppletUiViewV1,
|
|
21
|
+
type AppletViewerTokenV1,
|
|
22
|
+
} from "@frockbot/kernel-contracts";
|
|
23
|
+
|
|
24
|
+
/** Exactly what the shell's hosted transport offers, and nothing more. */
|
|
25
|
+
export type AppletsHostedRequest = (
|
|
26
|
+
path: string,
|
|
27
|
+
method?: "GET" | "POST",
|
|
28
|
+
body?: string,
|
|
29
|
+
) => Promise<unknown>;
|
|
30
|
+
|
|
31
|
+
const applet = (appletId: string) => encodeURIComponent(appletId);
|
|
32
|
+
|
|
33
|
+
export async function readAppletList(
|
|
34
|
+
request: AppletsHostedRequest,
|
|
35
|
+
): Promise<AppletSummaryV1[]> {
|
|
36
|
+
return decodeAppletListViewV1(await request("/api/applets")).applets;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function readAppletViewerToken(
|
|
40
|
+
request: AppletsHostedRequest,
|
|
41
|
+
appletId: string,
|
|
42
|
+
): Promise<AppletViewerTokenV1> {
|
|
43
|
+
return decodeAppletViewerTokenV1(
|
|
44
|
+
await request(`/api/applets/${applet(appletId)}/token`),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readAppletUi(
|
|
49
|
+
request: AppletsHostedRequest,
|
|
50
|
+
appletId: string,
|
|
51
|
+
): Promise<AppletUiViewV1> {
|
|
52
|
+
return decodeAppletUiViewV1(
|
|
53
|
+
await request(`/api/applets/${applet(appletId)}/ui`),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* The canvas's two Workspace-backed reads are Bot-scoped in the URL and
|
|
59
|
+
* User-scoped in what they answer: the Applets root belongs to the User, and
|
|
60
|
+
* the Bot in the path only names the Durable Object that holds the Workspace
|
|
61
|
+
* binding. Reading them wakes no Computer.
|
|
62
|
+
*/
|
|
63
|
+
export async function readAppletSource(
|
|
64
|
+
request: AppletsHostedRequest,
|
|
65
|
+
botId: string,
|
|
66
|
+
appletId: string,
|
|
67
|
+
): Promise<AppletSourceViewV1> {
|
|
68
|
+
return decodeAppletSourceViewV1(
|
|
69
|
+
await request(
|
|
70
|
+
`/api/bots/${encodeURIComponent(botId)}/applets/${applet(appletId)}/source`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function readAppletBuild(
|
|
76
|
+
request: AppletsHostedRequest,
|
|
77
|
+
botId: string,
|
|
78
|
+
appletId: string,
|
|
79
|
+
): Promise<AppletBuildViewV1> {
|
|
80
|
+
return decodeAppletBuildViewV1(
|
|
81
|
+
await request(
|
|
82
|
+
`/api/bots/${encodeURIComponent(botId)}/applets/${applet(appletId)}/build`,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function readFocusedAppletId(
|
|
88
|
+
request: AppletsHostedRequest,
|
|
89
|
+
botId: string,
|
|
90
|
+
): Promise<string | null> {
|
|
91
|
+
return decodeAppletFocusViewV1(
|
|
92
|
+
await request(`/api/bots/${encodeURIComponent(botId)}/applets/focus`),
|
|
93
|
+
).appletId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Records the Session's focused Applet and returns what the backend recorded,
|
|
98
|
+
* never what the click asked for: a focus the backend refused reads back as
|
|
99
|
+
* the focus it kept.
|
|
100
|
+
*/
|
|
101
|
+
export async function writeFocusedAppletId(
|
|
102
|
+
request: AppletsHostedRequest,
|
|
103
|
+
botId: string,
|
|
104
|
+
appletId: string | null,
|
|
105
|
+
): Promise<string | null> {
|
|
106
|
+
return decodeAppletFocusViewV1(
|
|
107
|
+
await request(
|
|
108
|
+
`/api/bots/${encodeURIComponent(botId)}/applets/focus`,
|
|
109
|
+
"POST",
|
|
110
|
+
JSON.stringify({ schemaVersion: 1, appletId }),
|
|
111
|
+
),
|
|
112
|
+
).appletId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The file the canvas shows while a Bot is writing an Applet: the one that
|
|
117
|
+
* changed most recently, falling back to the first path in sorted order so a
|
|
118
|
+
* store with no timestamps still opens on something.
|
|
119
|
+
*/
|
|
120
|
+
export function mostRecentlyChangedFileV1(
|
|
121
|
+
source: AppletSourceViewV1 | undefined,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
if (!source || source.files.length === 0) return undefined;
|
|
124
|
+
// A tie on time — a fresh scaffold, one sync — opens on the file a Bot edits
|
|
125
|
+
// first, not on the README the alphabet would pick.
|
|
126
|
+
const preferred = ["server.ts", "ui.tsx"];
|
|
127
|
+
const ordered = source.files.toSorted((left, right) => {
|
|
128
|
+
const leftAt = left.changedAt ?? "";
|
|
129
|
+
const rightAt = right.changedAt ?? "";
|
|
130
|
+
if (leftAt !== rightAt) return rightAt.localeCompare(leftAt);
|
|
131
|
+
const leftRank = preferred.indexOf(left.path);
|
|
132
|
+
const rightRank = preferred.indexOf(right.path);
|
|
133
|
+
const rank = (value: number) => (value < 0 ? preferred.length : value);
|
|
134
|
+
return (
|
|
135
|
+
rank(leftRank) - rank(rightRank) || left.path.localeCompare(right.path)
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
return ordered[0]?.path;
|
|
139
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `applets` feed a bridge v2 page receives.
|
|
3
|
+
*
|
|
4
|
+
* This is a projection of what the shell has already read, never a second
|
|
5
|
+
* source of truth: the backend is the authority for the list, the focus, and
|
|
6
|
+
* the viewer credential, and a page that is handed a stale generation simply
|
|
7
|
+
* reconnects when the next feed arrives.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
PACKAGE_IFRAME_FOCUS_TOOL_V2,
|
|
11
|
+
type PackageIframeAppletsStateV2,
|
|
12
|
+
type PackageIframeCatalogV1,
|
|
13
|
+
type AppletSummaryV1,
|
|
14
|
+
} from "@frockbot/kernel-contracts";
|
|
15
|
+
import type { FrockBotWebData } from "../shared.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether this Bot's Composition has Applets in it at all.
|
|
19
|
+
*
|
|
20
|
+
* Derived from manifest facts — a Package declaring the Applet focus tool —
|
|
21
|
+
* never from a Package id. A deployment or a User without the Applets Package
|
|
22
|
+
* has no Applet routes, and the shell must not ask for them: an absent
|
|
23
|
+
* capability is silence, not a failed request.
|
|
24
|
+
*/
|
|
25
|
+
export function appletsAvailableV1(
|
|
26
|
+
catalog: PackageIframeCatalogV1 | undefined,
|
|
27
|
+
): boolean {
|
|
28
|
+
return (catalog?.contributions ?? []).some((contribution) =>
|
|
29
|
+
contribution.declaredTools.includes(PACKAGE_IFRAME_FOCUS_TOOL_V2),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function focusedAppletSummaryV1(
|
|
34
|
+
web: Pick<FrockBotWebData, "applets" | "focusedAppletId">,
|
|
35
|
+
): AppletSummaryV1 | null {
|
|
36
|
+
const appletId = web.focusedAppletId;
|
|
37
|
+
if (!appletId) return null;
|
|
38
|
+
return web.applets.find((applet) => applet.appletId === appletId) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function appletsBridgeStateV2(
|
|
42
|
+
web: Pick<
|
|
43
|
+
FrockBotWebData,
|
|
44
|
+
"applets" | "focusedAppletId" | "appletViewer" | "appletBuild"
|
|
45
|
+
>,
|
|
46
|
+
): PackageIframeAppletsStateV2 {
|
|
47
|
+
const viewer = web.appletViewer;
|
|
48
|
+
return {
|
|
49
|
+
focused: focusedAppletSummaryV1(web),
|
|
50
|
+
list: web.applets,
|
|
51
|
+
viewer:
|
|
52
|
+
viewer && viewer.appletId === web.focusedAppletId
|
|
53
|
+
? {
|
|
54
|
+
token: viewer.token,
|
|
55
|
+
socketUrl: viewer.socketUrl,
|
|
56
|
+
uiUrl: viewer.uiUrl,
|
|
57
|
+
generationId: viewer.generationId,
|
|
58
|
+
}
|
|
59
|
+
: null,
|
|
60
|
+
// The source stays out of the feed: the shell draws the code view itself,
|
|
61
|
+
// and a source tree would overflow the bridge's 64 KB message bound.
|
|
62
|
+
...(web.appletBuild === undefined ? {} : { build: web.appletBuild }),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/client/index.test.ts
CHANGED
|
@@ -408,13 +408,19 @@ describe("Bot selection", () => {
|
|
|
408
408
|
packageId: "weather-page",
|
|
409
409
|
displayName: "Sydney Weather",
|
|
410
410
|
provenance: "Bot-authored" as const,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
411
|
+
pages: [
|
|
412
|
+
{
|
|
413
|
+
id: "main",
|
|
414
|
+
artifact: {
|
|
415
|
+
contentHash: "a".repeat(64),
|
|
416
|
+
size: 1,
|
|
417
|
+
mediaType: "text/html" as const,
|
|
418
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
419
|
+
},
|
|
420
|
+
mounts: [{ slot: "frockbot.bot-settings-sections", order: 20 }],
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
entries: [],
|
|
418
424
|
declaredTools: ["weather_lookup"],
|
|
419
425
|
};
|
|
420
426
|
const requests: Array<{
|
|
@@ -968,6 +974,52 @@ describe("Bot selection", () => {
|
|
|
968
974
|
});
|
|
969
975
|
|
|
970
976
|
describe("detached Turn projection", () => {
|
|
977
|
+
test("shows a dynamic call as its namespace/tool with inner arguments", () => {
|
|
978
|
+
const messages: Parameters<typeof projectCompletedRuns>[0] = [];
|
|
979
|
+
projectCompletedRuns(
|
|
980
|
+
messages,
|
|
981
|
+
[],
|
|
982
|
+
[
|
|
983
|
+
{
|
|
984
|
+
runId: "run-dynamic",
|
|
985
|
+
input: "Find open issues",
|
|
986
|
+
status: "completed",
|
|
987
|
+
responseText: "Done",
|
|
988
|
+
events: [
|
|
989
|
+
{
|
|
990
|
+
type: "tool/call",
|
|
991
|
+
call: {
|
|
992
|
+
id: "tool-1",
|
|
993
|
+
name: "call_dynamic_tool",
|
|
994
|
+
input: {
|
|
995
|
+
namespace: "user-Github--acme",
|
|
996
|
+
toolName: "search_issues",
|
|
997
|
+
argumentsJson: '{"query":"is:open"}',
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
type: "tool/result",
|
|
1003
|
+
callId: "tool-1",
|
|
1004
|
+
content: "[]",
|
|
1005
|
+
isError: false,
|
|
1006
|
+
},
|
|
1007
|
+
],
|
|
1008
|
+
},
|
|
1009
|
+
],
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
expect(messages[1]?.tools).toEqual([
|
|
1013
|
+
{
|
|
1014
|
+
id: "tool-1",
|
|
1015
|
+
name: "user-Github--acme/search_issues",
|
|
1016
|
+
input: { query: "is:open" },
|
|
1017
|
+
status: "completed",
|
|
1018
|
+
text: "[]",
|
|
1019
|
+
},
|
|
1020
|
+
]);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
971
1023
|
test("projects a completed run before it can be acknowledged", () => {
|
|
972
1024
|
const messages: Parameters<typeof projectCompletedRuns>[0] = [];
|
|
973
1025
|
const projected = projectCompletedRuns(
|
|
@@ -2645,3 +2697,165 @@ describe("Connection operation reconciliation", () => {
|
|
|
2645
2697
|
expect(commandIds[1]).not.toBe(commandIds[0]);
|
|
2646
2698
|
});
|
|
2647
2699
|
});
|
|
2700
|
+
|
|
2701
|
+
describe("a message sent while a Turn is running", () => {
|
|
2702
|
+
test("is accepted, and carries the intent to supersede what is running", async () => {
|
|
2703
|
+
Object.defineProperty(globalThis, "window", {
|
|
2704
|
+
configurable: true,
|
|
2705
|
+
value: {
|
|
2706
|
+
location: { href: "https://app.example/?bot=primary" },
|
|
2707
|
+
history: { replaceState: () => undefined },
|
|
2708
|
+
},
|
|
2709
|
+
});
|
|
2710
|
+
let provided: Ref<FrockBotWebData> | undefined;
|
|
2711
|
+
const sent: {
|
|
2712
|
+
text: string;
|
|
2713
|
+
supersedes?: { runId?: string };
|
|
2714
|
+
}[] = [];
|
|
2715
|
+
let releaseFirst!: () => void;
|
|
2716
|
+
const firstTurn = new Promise<void>((resolve) => {
|
|
2717
|
+
releaseFirst = resolve;
|
|
2718
|
+
});
|
|
2719
|
+
await shellClientPlugin({
|
|
2720
|
+
transport: {
|
|
2721
|
+
turn: async (_botId, text, _signal, commandId, _skills, supersedes) => {
|
|
2722
|
+
sent.push({ text, ...(supersedes ? { supersedes } : {}) });
|
|
2723
|
+
if (sent.length === 1) await firstTurn;
|
|
2724
|
+
return { runId: commandId, text: `answer to ${text}`, events: [] };
|
|
2725
|
+
},
|
|
2726
|
+
readConfiguration: () =>
|
|
2727
|
+
Promise.resolve(initializeBotSettingsV1("primary")),
|
|
2728
|
+
listRuns: () => Promise.resolve([]),
|
|
2729
|
+
listNotifications: () => Promise.resolve([]),
|
|
2730
|
+
},
|
|
2731
|
+
slot: () => () => {},
|
|
2732
|
+
inject: () => {
|
|
2733
|
+
throw new Error("unexpected client provider injection");
|
|
2734
|
+
},
|
|
2735
|
+
provide: (_key, value) => {
|
|
2736
|
+
provided = value as Ref<FrockBotWebData>;
|
|
2737
|
+
return () => {};
|
|
2738
|
+
},
|
|
2739
|
+
});
|
|
2740
|
+
if (!provided) throw new Error("shell data was not provided");
|
|
2741
|
+
provided.value.activeBotId = "primary";
|
|
2742
|
+
provided.value.composerContext = "primary";
|
|
2743
|
+
|
|
2744
|
+
const first = provided.value.sendPrompt("first");
|
|
2745
|
+
await Promise.resolve();
|
|
2746
|
+
const runningRunId = provided.value.activeRunId;
|
|
2747
|
+
expect(runningRunId).toBeString();
|
|
2748
|
+
|
|
2749
|
+
// The composer is open while a Turn runs, and what it sends replaces it.
|
|
2750
|
+
const second = provided.value.sendPrompt("second");
|
|
2751
|
+
releaseFirst();
|
|
2752
|
+
expect(await first).toMatchObject({ accepted: true });
|
|
2753
|
+
expect(await second).toMatchObject({ accepted: true });
|
|
2754
|
+
|
|
2755
|
+
expect(sent.map((entry) => entry.text).sort()).toEqual(["first", "second"]);
|
|
2756
|
+
// Every send carries the intent. The first had observed no run, so it
|
|
2757
|
+
// carries no provenance — and that is exactly the send that used to be
|
|
2758
|
+
// refused when a person typed faster than the client could observe the
|
|
2759
|
+
// Turn it had just started.
|
|
2760
|
+
expect(sent.find((entry) => entry.text === "first")?.supersedes).toEqual(
|
|
2761
|
+
{},
|
|
2762
|
+
);
|
|
2763
|
+
expect(sent.find((entry) => entry.text === "second")?.supersedes).toEqual({
|
|
2764
|
+
runId: runningRunId!,
|
|
2765
|
+
});
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
test("greys the queued Turn, and un-greys it when it starts", () => {
|
|
2769
|
+
const state: Pick<
|
|
2770
|
+
FrockBotWebData,
|
|
2771
|
+
"messages" | "activeRunId" | "runningRunId" | "activeRun"
|
|
2772
|
+
> = { messages: [] };
|
|
2773
|
+
|
|
2774
|
+
projectDurableRuns(
|
|
2775
|
+
state,
|
|
2776
|
+
[],
|
|
2777
|
+
[
|
|
2778
|
+
{ runId: "run-1", input: "first", events: [], status: "running" },
|
|
2779
|
+
{
|
|
2780
|
+
runId: "run-2",
|
|
2781
|
+
input: "second",
|
|
2782
|
+
events: [],
|
|
2783
|
+
status: "running",
|
|
2784
|
+
queued: true,
|
|
2785
|
+
},
|
|
2786
|
+
],
|
|
2787
|
+
);
|
|
2788
|
+
|
|
2789
|
+
// Ordinary messages; the waiting one is simply held back.
|
|
2790
|
+
expect(state.messages.map((message) => message.pending)).toEqual([
|
|
2791
|
+
undefined,
|
|
2792
|
+
undefined,
|
|
2793
|
+
true,
|
|
2794
|
+
true,
|
|
2795
|
+
]);
|
|
2796
|
+
// A new message supersedes the newest unsettled Turn; Stop cancels the one
|
|
2797
|
+
// that is actually executing.
|
|
2798
|
+
expect(state.activeRunId).toBe("run-2");
|
|
2799
|
+
expect(state.runningRunId).toBe("run-1");
|
|
2800
|
+
|
|
2801
|
+
projectDurableRuns(
|
|
2802
|
+
state,
|
|
2803
|
+
[],
|
|
2804
|
+
[
|
|
2805
|
+
{
|
|
2806
|
+
runId: "run-1",
|
|
2807
|
+
input: "first",
|
|
2808
|
+
events: [],
|
|
2809
|
+
status: "superseded",
|
|
2810
|
+
failure: "Interrupted by your next message.",
|
|
2811
|
+
},
|
|
2812
|
+
{ runId: "run-2", input: "second", events: [], status: "running" },
|
|
2813
|
+
],
|
|
2814
|
+
);
|
|
2815
|
+
|
|
2816
|
+
expect(state.messages.every((message) => !message.pending)).toBe(true);
|
|
2817
|
+
expect(state.activeRunId).toBe("run-2");
|
|
2818
|
+
expect(state.runningRunId).toBe("run-2");
|
|
2819
|
+
// The superseded Turn keeps the quiet treatment a stopped one gets.
|
|
2820
|
+
expect(state.messages[1]).toMatchObject({
|
|
2821
|
+
status: "aborted",
|
|
2822
|
+
text: "Interrupted by your next message.",
|
|
2823
|
+
});
|
|
2824
|
+
});
|
|
2825
|
+
|
|
2826
|
+
test("a reload reconstructs the greyed state from durable runs alone", () => {
|
|
2827
|
+
const reloaded: Pick<
|
|
2828
|
+
FrockBotWebData,
|
|
2829
|
+
"messages" | "activeRunId" | "runningRunId" | "activeRun"
|
|
2830
|
+
> = { messages: [] };
|
|
2831
|
+
|
|
2832
|
+
projectDurableRuns(
|
|
2833
|
+
reloaded,
|
|
2834
|
+
[],
|
|
2835
|
+
[
|
|
2836
|
+
{
|
|
2837
|
+
runId: "run-1",
|
|
2838
|
+
input: "first",
|
|
2839
|
+
events: [],
|
|
2840
|
+
status: "superseded",
|
|
2841
|
+
failure: "Interrupted by your next message.",
|
|
2842
|
+
},
|
|
2843
|
+
{
|
|
2844
|
+
runId: "run-2",
|
|
2845
|
+
input: "second",
|
|
2846
|
+
events: [],
|
|
2847
|
+
status: "running",
|
|
2848
|
+
queued: true,
|
|
2849
|
+
},
|
|
2850
|
+
],
|
|
2851
|
+
);
|
|
2852
|
+
|
|
2853
|
+
expect(reloaded.messages[2]).toMatchObject({
|
|
2854
|
+
role: "user",
|
|
2855
|
+
text: "second",
|
|
2856
|
+
pending: true,
|
|
2857
|
+
});
|
|
2858
|
+
expect(reloaded.runningRunId).toBeUndefined();
|
|
2859
|
+
expect(reloaded.activeRunId).toBe("run-2");
|
|
2860
|
+
});
|
|
2861
|
+
});
|