@superblocksteam/gateway 2.0.167-next.0 → 2.0.169-next.0
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/README.md +163 -28
- package/dist/agents/resolve-orchestrator-url.js +1 -2
- package/dist/agents/resolve-orchestrator-url.js.map +1 -1
- package/dist/capabilities/lifecycle.d.ts +3 -0
- package/dist/capabilities/lifecycle.js +330 -128
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/live-state-attaches.d.ts +26 -0
- package/dist/capabilities/live-state-attaches.js +41 -0
- package/dist/capabilities/live-state-attaches.js.map +1 -0
- package/dist/capabilities/tool-permission-reply.d.ts +28 -0
- package/dist/capabilities/tool-permission-reply.js +55 -0
- package/dist/capabilities/tool-permission-reply.js.map +1 -0
- package/dist/capabilities/types.d.ts +19 -1
- package/dist/capabilities/types.js.map +1 -1
- package/dist/deadline.d.ts +6 -0
- package/dist/deadline.js +23 -0
- package/dist/deadline.js.map +1 -0
- package/dist/deps.d.ts +7 -0
- package/dist/health/readiness.js +1 -17
- package/dist/health/readiness.js.map +1 -1
- package/dist/integrations/filters.js +2 -2
- package/dist/integrations/filters.js.map +1 -1
- package/dist/orchestrator/client.d.ts +2 -1
- package/dist/orchestrator/client.js +4 -2
- package/dist/orchestrator/client.js.map +1 -1
- package/dist/sabs/app-state.d.ts +1 -0
- package/dist/sabs/app-state.js +9 -0
- package/dist/sabs/app-state.js.map +1 -1
- package/dist/sabs/awaited-decision.d.ts +24 -1
- package/dist/sabs/awaited-decision.js +40 -9
- package/dist/sabs/awaited-decision.js.map +1 -1
- package/dist/sabs/editor-client-methods.d.ts +21 -10
- package/dist/sabs/editor-client-methods.js +48 -24
- package/dist/sabs/editor-client-methods.js.map +1 -1
- package/dist/sabs/session-peer.d.ts +47 -14
- package/dist/sabs/session-peer.js.map +1 -1
- package/dist/sabs/turn-collector.d.ts +27 -1
- package/dist/sabs/turn-collector.js +38 -2
- package/dist/sabs/turn-collector.js.map +1 -1
- package/dist/sabs/websocket-session-peer.d.ts +27 -3
- package/dist/sabs/websocket-session-peer.js +476 -76
- package/dist/sabs/websocket-session-peer.js.map +1 -1
- package/dist/server/client.d.ts +5 -0
- package/dist/server/client.js +9 -3
- package/dist/server/client.js.map +1 -1
- package/dist/start.d.ts +13 -2
- package/dist/start.js +112 -12
- package/dist/start.js.map +1 -1
- package/dist/telemetry/metrics.js +8 -21
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/runtime.d.ts +5 -0
- package/dist/telemetry/runtime.js +22 -8
- package/dist/telemetry/runtime.js.map +1 -1
- package/dist/transports/mcp/admin-tools.d.ts +2 -1
- package/dist/transports/mcp/admin-tools.js +19 -2
- package/dist/transports/mcp/admin-tools.js.map +1 -1
- package/dist/transports/mcp/client-presentation.d.ts +9 -1
- package/dist/transports/mcp/client-presentation.js +10 -0
- package/dist/transports/mcp/client-presentation.js.map +1 -1
- package/dist/transports/mcp/decision-card-html.d.ts +4 -2
- package/dist/transports/mcp/decision-card-html.js +107 -25
- package/dist/transports/mcp/decision-card-html.js.map +1 -1
- package/dist/transports/mcp/decision-elicitation.d.ts +3 -2
- package/dist/transports/mcp/decision-elicitation.js +67 -2
- package/dist/transports/mcp/decision-elicitation.js.map +1 -1
- package/dist/transports/mcp/format-tool-content.d.ts +7 -0
- package/dist/transports/mcp/format-tool-content.js +3 -3
- package/dist/transports/mcp/format-tool-content.js.map +1 -1
- package/dist/transports/mcp/instructions/result.js +1 -0
- package/dist/transports/mcp/instructions/result.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/ask-user.d.ts +2 -1
- package/dist/transports/mcp/instructions/tools/ask-user.js +9 -2
- package/dist/transports/mcp/instructions/tools/ask-user.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/copy.d.ts +5 -2
- package/dist/transports/mcp/instructions/tools/copy.js +20 -5
- package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.d.ts +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js +11 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
- package/dist/transports/mcp/mount.d.ts +2 -3
- package/dist/transports/mcp/mount.js +395 -112
- package/dist/transports/mcp/mount.js.map +1 -1
- package/dist/transports/mcp/plan-approval.d.ts +23 -13
- package/dist/transports/mcp/plan-approval.js +39 -26
- package/dist/transports/mcp/plan-approval.js.map +1 -1
- package/package.json +5 -5
|
@@ -5,6 +5,7 @@ import { detectAttachmentArchiveType, getAttachmentMimeTypeFromFileName, getText
|
|
|
5
5
|
import { buildBrowserDriverContract, HOST_BROWSER_TOOLS_COWORK, needsBrowserDriverError, } from "../capture/browser-contract.js";
|
|
6
6
|
import { withCoworkEditorHost } from "../capture/cowork-editor-url.js";
|
|
7
7
|
import { resolveCaptureMode } from "../capture/mode.js";
|
|
8
|
+
import { withDeadline } from "../deadline.js";
|
|
8
9
|
import { gatewayDebug } from "../debug.js";
|
|
9
10
|
import { playwrightChromiumPresent } from "../playwright/ensure-chromium.js";
|
|
10
11
|
import { isTrustedEmbedOrigin, } from "../preview/capture-screenshot.js";
|
|
@@ -13,6 +14,7 @@ import { PromptNotSentError, } from "../sabs/session-peer.js";
|
|
|
13
14
|
import { ServerHttpError } from "../server/client.js";
|
|
14
15
|
import { IdentityUnavailableError } from "../server/identity-reads.js";
|
|
15
16
|
import { archiveSourceFiles } from "./source-files-archive.js";
|
|
17
|
+
import { TOOL_PERMISSION_ANSWERS, ToolPermissionReply, } from "./tool-permission-reply.js";
|
|
16
18
|
import { buildStatusOf, progressStatusOf } from "./types.js";
|
|
17
19
|
import { IMPORT_ZIP_MAX_BYTES } from "./types.js";
|
|
18
20
|
export { IMPORT_ZIP_MAX_BYTES } from "./types.js";
|
|
@@ -66,8 +68,8 @@ async function captureModeOf(ctx) {
|
|
|
66
68
|
hostMcpConfigured: ctx.config.captureHostMcp,
|
|
67
69
|
});
|
|
68
70
|
}
|
|
69
|
-
function libraryCaptureInput(ctx, applicationId, branch) {
|
|
70
|
-
const context = ctx.sessionPeer.libraryBrowserContext(applicationId, branch);
|
|
71
|
+
async function libraryCaptureInput(ctx, applicationId, branch) {
|
|
72
|
+
const context = await ctx.sessionPeer.libraryBrowserContext(applicationId, branch);
|
|
71
73
|
if (!context) {
|
|
72
74
|
return undefined;
|
|
73
75
|
}
|
|
@@ -115,30 +117,71 @@ function libraryCaptureInput(ctx, applicationId, branch) {
|
|
|
115
117
|
},
|
|
116
118
|
};
|
|
117
119
|
}
|
|
120
|
+
function heldSocketFor(ctx, applicationId, branch) {
|
|
121
|
+
const held = ctx.sessionPeer.heldContext(applicationId);
|
|
122
|
+
if (!held) {
|
|
123
|
+
return "none";
|
|
124
|
+
}
|
|
125
|
+
return held.branch === branch ? "this_branch" : "other_branch";
|
|
126
|
+
}
|
|
118
127
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
128
|
+
* `branch` is the branch the read resolved to, undefined for the default
|
|
129
|
+
* branch: a socket held for any other branch cannot answer for this read.
|
|
130
|
+
*
|
|
131
|
+
* A cold live edit can take minutes to attach. Past `deadlineAtMs` the read
|
|
132
|
+
* answers from what the gateway already knows, and the attach carries on so
|
|
133
|
+
* the next read finds the session held.
|
|
121
134
|
*/
|
|
122
|
-
async function
|
|
123
|
-
const
|
|
124
|
-
if (existing) {
|
|
125
|
-
return existing;
|
|
126
|
-
}
|
|
127
|
-
if (ctx.appState.pendingStart({
|
|
135
|
+
async function ensureLiveState(ctx, principal, applicationId, branch, deadlineAtMs) {
|
|
136
|
+
const scope = {
|
|
128
137
|
applicationId,
|
|
129
|
-
|
|
138
|
+
branch,
|
|
130
139
|
organizationId: principal.organizationId,
|
|
131
140
|
userId: principal.userId,
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
};
|
|
142
|
+
const held = ctx.sessionPeer.heldContext(applicationId);
|
|
143
|
+
if ((held &&
|
|
144
|
+
held.branch === branch &&
|
|
145
|
+
!ctx.liveStateAttaches.isAttaching(scope)) ||
|
|
146
|
+
ctx.appState.pendingStart(scope) ||
|
|
147
|
+
ctx.sessionPeer.turnState(scope)?.phase === "running") {
|
|
148
|
+
return { kind: "ok", value: undefined };
|
|
149
|
+
}
|
|
150
|
+
const activeTurn = ctx.sessionPeer.turnState({ ...scope, branch: undefined });
|
|
151
|
+
if (activeTurn?.phase === "running" ||
|
|
152
|
+
ctx.appState.pendingStart({ ...scope, branch: undefined })) {
|
|
153
|
+
return {
|
|
137
154
|
applicationId,
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
code: "live_edit_unavailable",
|
|
156
|
+
kind: "error",
|
|
157
|
+
message: "Another branch is building. Wait for it to finish before reading this branch.",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return await withDeadline(ctx.liveStateAttaches.run(scope, () => attachLiveState(ctx, principal, scope)), Math.max(0, deadlineAtMs - nowMs(ctx)), () => ({ kind: "ok", value: undefined }));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Whether a read may answer from what this process knows when the editor
|
|
164
|
+
* state could not be refreshed. A running turn is followed on its own socket
|
|
165
|
+
* and a recorded dead start is this process's own account. Beyond those, only
|
|
166
|
+
* a decision or a finished turn can have been overtaken in the editor. With
|
|
167
|
+
* neither there is nothing a refresh could correct, and failing the read
|
|
168
|
+
* would turn another user's edit or a full sandbox pool into an error on an
|
|
169
|
+
* app that is simply ready.
|
|
170
|
+
*/
|
|
171
|
+
function knownStateAnswersWithoutRefresh(ctx, scope) {
|
|
172
|
+
const turn = ctx.sessionPeer.turnState(scope);
|
|
173
|
+
if (turn?.phase === "running" || ctx.appState.failedStart(scope)) {
|
|
174
|
+
return true;
|
|
140
175
|
}
|
|
176
|
+
return (turn === undefined &&
|
|
177
|
+
!ctx.appState.pendingDecision(scope.applicationId, scope.branch) &&
|
|
178
|
+
!ctx.appState.pendingPlan(scope.applicationId, scope.branch));
|
|
179
|
+
}
|
|
180
|
+
async function attachLiveState(ctx, principal, scope) {
|
|
181
|
+
const { applicationId, branch } = scope;
|
|
141
182
|
try {
|
|
183
|
+
// TODO(AR-593): read-only tools reach this attach, which can start the
|
|
184
|
+
// caller's sandbox and take a free editor lock for the read hold.
|
|
142
185
|
const session = await openLiveEdit(ctx, {
|
|
143
186
|
apiKey: principal.apiKey,
|
|
144
187
|
applicationId,
|
|
@@ -175,17 +218,24 @@ async function ensureLibraryCaptureInput(ctx, principal, applicationId, branch)
|
|
|
175
218
|
});
|
|
176
219
|
}
|
|
177
220
|
catch (error) {
|
|
178
|
-
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
const answeredWithoutRefresh = knownStateAnswersWithoutRefresh(ctx, scope);
|
|
223
|
+
console.error("gateway live state refresh failed", {
|
|
224
|
+
answeredWithoutRefresh,
|
|
179
225
|
applicationId,
|
|
180
|
-
message
|
|
226
|
+
message,
|
|
181
227
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
228
|
+
if (answeredWithoutRefresh) {
|
|
229
|
+
return { kind: "ok", value: undefined };
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
applicationId,
|
|
233
|
+
code: "live_edit_unavailable",
|
|
234
|
+
kind: "error",
|
|
235
|
+
message: `Could not refresh the editor state: ${message}`,
|
|
236
|
+
};
|
|
187
237
|
}
|
|
188
|
-
return
|
|
238
|
+
return { kind: "ok", value: undefined };
|
|
189
239
|
}
|
|
190
240
|
async function captureLiveLibraryIfNeeded(ctx, applicationId, live) {
|
|
191
241
|
if (!live) {
|
|
@@ -623,14 +673,6 @@ async function waitForDeployLive(ctx, input) {
|
|
|
623
673
|
* see what they built.
|
|
624
674
|
*/
|
|
625
675
|
const NEXT_STEPS = "Inspect it with get_app, or publish it with publish_app";
|
|
626
|
-
/**
|
|
627
|
-
* Which side of a waiting turn owes the next move.
|
|
628
|
-
*
|
|
629
|
-
* A stream hiccup can deliver the plan as chat text instead of a plan card,
|
|
630
|
-
* leaving needs_decision with no structured decision. The turn is waiting on a
|
|
631
|
-
* human either way, so the absence of a plan reads as a question, never as
|
|
632
|
-
* nothing to answer.
|
|
633
|
-
*/
|
|
634
676
|
function pendingActionForDecision(decision) {
|
|
635
677
|
if (!decision) {
|
|
636
678
|
return "decide_in_editor";
|
|
@@ -639,6 +681,7 @@ function pendingActionForDecision(decision) {
|
|
|
639
681
|
case "missing_integrations":
|
|
640
682
|
return "create_integration";
|
|
641
683
|
case "multi_choice":
|
|
684
|
+
case "tool_permission":
|
|
642
685
|
return "answer_question";
|
|
643
686
|
case "plan":
|
|
644
687
|
return "approve_plan";
|
|
@@ -670,6 +713,9 @@ function decisionSummary(decision) {
|
|
|
670
713
|
if (decision.kind === "multi_choice") {
|
|
671
714
|
return "Superblocks asked the user a question and stopped.";
|
|
672
715
|
}
|
|
716
|
+
if (decision.kind === "tool_permission") {
|
|
717
|
+
return "Superblocks needs a user approval decision before it can continue.";
|
|
718
|
+
}
|
|
673
719
|
const unhandled = decision;
|
|
674
720
|
return unhandled;
|
|
675
721
|
}
|
|
@@ -1265,7 +1311,30 @@ async function runAcknowledgedStart(ctx, args) {
|
|
|
1265
1311
|
// prompt went out.
|
|
1266
1312
|
args.approvalSettlement?.onNotSent();
|
|
1267
1313
|
}
|
|
1268
|
-
if (result.kind === "error" &&
|
|
1314
|
+
if (result.kind === "error" &&
|
|
1315
|
+
(result.code === "plan_approval_stale" ||
|
|
1316
|
+
result.code === "tool_permission_pending" ||
|
|
1317
|
+
result.code === "tool_permission_stale")) {
|
|
1318
|
+
const currentDecision = ctx.appState.pendingDecision(args.applicationId, args.branch);
|
|
1319
|
+
// A cleared decision leaves the live edit healthy: only the approval
|
|
1320
|
+
// target went away, so the turn reads as ended, not terminal.
|
|
1321
|
+
rememberFailedStart(ctx, {
|
|
1322
|
+
applicationId: args.applicationId,
|
|
1323
|
+
branch: args.branch,
|
|
1324
|
+
...(currentDecision ? { decision: currentDecision.decision } : {}),
|
|
1325
|
+
error: new Error(result.message),
|
|
1326
|
+
generationStatus: currentDecision ? "needs_decision" : "no_changes",
|
|
1327
|
+
idempotencyKey: args.idempotencyKey,
|
|
1328
|
+
pendingStartRequired: true,
|
|
1329
|
+
pendingStartToken: args.pendingStartToken,
|
|
1330
|
+
principal: args.principal,
|
|
1331
|
+
reason: currentDecision
|
|
1332
|
+
? result.message
|
|
1333
|
+
: "The approval request is no longer pending, so this response was not sent to Superblocks.",
|
|
1334
|
+
revision: args.pendingStartToken,
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
else if (result.kind === "error" && result.code === "reinvoke_required") {
|
|
1269
1338
|
failed(new Error(result.message), `Authentication completed before Superblocks accepted the prompt for application ${args.applicationId}. Call edit_app with that applicationId and the same prompt to continue.`, "live_edit_terminated", "retry_build");
|
|
1270
1339
|
}
|
|
1271
1340
|
else if (result.kind === "auth_required" ||
|
|
@@ -1704,6 +1773,10 @@ function nowMs(ctx) {
|
|
|
1704
1773
|
function hostCallBudgetMs(ctx, requestedMs) {
|
|
1705
1774
|
return Math.max(0, Math.min(requestedMs, ctx.config.hostCallCeilingMs));
|
|
1706
1775
|
}
|
|
1776
|
+
/** When a call that starts now has to have answered by. */
|
|
1777
|
+
function hostCallDeadlineAtMs(ctx) {
|
|
1778
|
+
return nowMs(ctx) + Math.max(0, ctx.config.hostCallCeilingMs);
|
|
1779
|
+
}
|
|
1707
1780
|
/**
|
|
1708
1781
|
* How long the next turn must collect activity for before it may answer.
|
|
1709
1782
|
*
|
|
@@ -1868,7 +1941,7 @@ async function awaitTurns(ctx, requestedBranch, principal, applicationId, hostDe
|
|
|
1868
1941
|
}
|
|
1869
1942
|
}
|
|
1870
1943
|
export async function checkAppProgress(ctx, input) {
|
|
1871
|
-
const hostDeadlineAtMs =
|
|
1944
|
+
const hostDeadlineAtMs = hostCallDeadlineAtMs(ctx);
|
|
1872
1945
|
const principalResult = await ensurePrincipal(ctx);
|
|
1873
1946
|
if (principalResult.kind !== "ok") {
|
|
1874
1947
|
return principalResult;
|
|
@@ -1886,11 +1959,18 @@ export async function checkAppProgress(ctx, input) {
|
|
|
1886
1959
|
if (ctx.signal?.aborted) {
|
|
1887
1960
|
return namingApplication(applicationId, cancelled("check_app_progress"));
|
|
1888
1961
|
}
|
|
1962
|
+
const refreshed = await ensureLiveState(ctx, principal, applicationId, currentTurnBranch(ctx, principal, applicationId, requestedBranch), hostDeadlineAtMs);
|
|
1963
|
+
if (refreshed.kind !== "ok") {
|
|
1964
|
+
return refreshed;
|
|
1965
|
+
}
|
|
1889
1966
|
const updateResult = await awaitTurns(ctx, requestedBranch, principal, applicationId, hostDeadlineAtMs);
|
|
1890
1967
|
if (updateResult.kind !== "ok") {
|
|
1891
1968
|
return updateResult;
|
|
1892
1969
|
}
|
|
1893
1970
|
const update = updateResult.value;
|
|
1971
|
+
const untracked = update.status === "unknown"
|
|
1972
|
+
? await untrackedStatus(ctx, principal, applicationId, requestedBranch)
|
|
1973
|
+
: undefined;
|
|
1894
1974
|
const elapsedSeconds = Math.round(update.elapsedMs / 1000);
|
|
1895
1975
|
const saidSomething = update.activity.length > 0;
|
|
1896
1976
|
const activity = update.status === "running" && !saidSomething
|
|
@@ -1899,7 +1979,7 @@ export async function checkAppProgress(ctx, input) {
|
|
|
1899
1979
|
for (const message of activity) {
|
|
1900
1980
|
ctx.onProgress?.({ message, type: "progress" });
|
|
1901
1981
|
}
|
|
1902
|
-
const status = progressStatusOf(update);
|
|
1982
|
+
const status = untracked?.status ?? progressStatusOf(update);
|
|
1903
1983
|
const terminal = status !== "building";
|
|
1904
1984
|
const turnSignals = await recordTurnSignals(ctx, {
|
|
1905
1985
|
appScope: { applicationId, organizationId: principal.organizationId },
|
|
@@ -1911,7 +1991,7 @@ export async function checkAppProgress(ctx, input) {
|
|
|
1911
1991
|
saidSomething,
|
|
1912
1992
|
terminal,
|
|
1913
1993
|
});
|
|
1914
|
-
const branch = update.branch ?? requestedBranch;
|
|
1994
|
+
const branch = update.branch ?? untracked?.branch ?? requestedBranch;
|
|
1915
1995
|
const base = {
|
|
1916
1996
|
...turnSignals,
|
|
1917
1997
|
activity,
|
|
@@ -1931,6 +2011,7 @@ export async function checkAppProgress(ctx, input) {
|
|
|
1931
2011
|
kind: "ok",
|
|
1932
2012
|
value: {
|
|
1933
2013
|
...base,
|
|
2014
|
+
...untracked,
|
|
1934
2015
|
status,
|
|
1935
2016
|
},
|
|
1936
2017
|
};
|
|
@@ -1949,21 +2030,16 @@ export async function checkAppProgress(ctx, input) {
|
|
|
1949
2030
|
...(update.checkpointWarning
|
|
1950
2031
|
? { checkpointWarning: update.checkpointWarning }
|
|
1951
2032
|
: {}),
|
|
1952
|
-
reply: update.reply,
|
|
1953
2033
|
...(update.decision ? { decision: update.decision } : {}),
|
|
1954
2034
|
generationStatus: update.generationStatus,
|
|
1955
|
-
...(update
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
? { pendingAction:
|
|
1959
|
-
:
|
|
1960
|
-
? { pendingAction: update.
|
|
1961
|
-
: update
|
|
1962
|
-
|
|
1963
|
-
? update.recoveryAction
|
|
1964
|
-
? { pendingAction: update.recoveryAction }
|
|
1965
|
-
: {}
|
|
1966
|
-
: {}),
|
|
2035
|
+
...(planQuestionOutcome(update) ?? {
|
|
2036
|
+
reply: update.reply,
|
|
2037
|
+
...(update.checkpointFailure
|
|
2038
|
+
? { pendingAction: "open_editor" }
|
|
2039
|
+
: status === "needs_decision"
|
|
2040
|
+
? { pendingAction: pendingActionForDecision(update.decision) }
|
|
2041
|
+
: emptyTurnRecovery(update)),
|
|
2042
|
+
}),
|
|
1967
2043
|
status,
|
|
1968
2044
|
viewStatus: update.viewStatus,
|
|
1969
2045
|
},
|
|
@@ -2079,10 +2155,55 @@ function stalePlanApproval(applicationId) {
|
|
|
2079
2155
|
function planApprovalChanged(approvedPlan, pendingPlan) {
|
|
2080
2156
|
return approvedPlan !== undefined && pendingPlan !== approvedPlan;
|
|
2081
2157
|
}
|
|
2158
|
+
// These messages stay host-neutral: the host-specific way to answer comes from
|
|
2159
|
+
// the tool's agentInstructions, which know whether the host can collect an
|
|
2160
|
+
// approval at all.
|
|
2161
|
+
function ambiguousToolPermissionReply(applicationId) {
|
|
2162
|
+
return {
|
|
2163
|
+
applicationId,
|
|
2164
|
+
kind: "error",
|
|
2165
|
+
code: "tool_permission_reply_ambiguous",
|
|
2166
|
+
message: `This reply to the pending permission is neither an approval nor a decline, so nothing was sent to Superblocks. Answer with permissionMessageId set to decision.messageId and ${TOOL_PERMISSION_ANSWERS}.`,
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
function pendingToolPermission(applicationId) {
|
|
2170
|
+
return {
|
|
2171
|
+
applicationId,
|
|
2172
|
+
kind: "error",
|
|
2173
|
+
code: "tool_permission_pending",
|
|
2174
|
+
message: `Superblocks is waiting on a permission request, so it cannot take a new prompt yet. Answer it with permissionMessageId set to decision.messageId: \`Decline: <the new instruction>\` to replace it with a new instruction, or ${TOOL_PERMISSION_ANSWERS}.`,
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
function staleToolPermissionResponse(applicationId) {
|
|
2178
|
+
return {
|
|
2179
|
+
applicationId,
|
|
2180
|
+
kind: "error",
|
|
2181
|
+
code: "tool_permission_stale",
|
|
2182
|
+
message: "This permission request is no longer current, so nothing was sent to Superblocks. Get the current request from check_app_progress and follow its instructions.",
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2082
2185
|
async function promptExistingApplication(ctx, args) {
|
|
2083
2186
|
const { applicationId, approvedPlan, input, principal } = args;
|
|
2084
2187
|
const branch = currentTurnBranch(ctx, principal, applicationId, input.branch);
|
|
2085
|
-
const
|
|
2188
|
+
const pendingInteractive = pendingInteractiveDecision(ctx, applicationId, branch);
|
|
2189
|
+
const answeringQuestion = Boolean(pendingInteractive);
|
|
2190
|
+
const answeringToolPermission = pendingInteractive?.kind === "tool_permission";
|
|
2191
|
+
const permissionMismatch = () => {
|
|
2192
|
+
const current = pendingInteractiveDecision(ctx, applicationId, branch);
|
|
2193
|
+
const pendingPermission = current?.kind === "tool_permission" ? current : undefined;
|
|
2194
|
+
if (input.permissionMessageId === undefined) {
|
|
2195
|
+
return pendingPermission
|
|
2196
|
+
? pendingToolPermission(applicationId)
|
|
2197
|
+
: undefined;
|
|
2198
|
+
}
|
|
2199
|
+
return pendingPermission?.messageId === input.permissionMessageId
|
|
2200
|
+
? undefined
|
|
2201
|
+
: staleToolPermissionResponse(applicationId);
|
|
2202
|
+
};
|
|
2203
|
+
const mismatchBeforeOpen = permissionMismatch();
|
|
2204
|
+
if (mismatchBeforeOpen) {
|
|
2205
|
+
return mismatchBeforeOpen;
|
|
2206
|
+
}
|
|
2086
2207
|
// Approve reuses the pending plan as the prompt. Every other path needs the
|
|
2087
2208
|
// caller's words - an empty or missing prompt would otherwise forward
|
|
2088
2209
|
// undefined into Clark. Answering a question is not a plan approval, even if
|
|
@@ -2095,6 +2216,12 @@ async function promptExistingApplication(ctx, args) {
|
|
|
2095
2216
|
message: 'edit_app needs a prompt, unless planAction is "approve" (Build it) for a pending plan.',
|
|
2096
2217
|
};
|
|
2097
2218
|
}
|
|
2219
|
+
const permissionReply = answeringToolPermission
|
|
2220
|
+
? ToolPermissionReply.parse(input.prompt ?? "")
|
|
2221
|
+
: undefined;
|
|
2222
|
+
if (permissionReply?.isAmbiguous) {
|
|
2223
|
+
return ambiguousToolPermissionReply(applicationId);
|
|
2224
|
+
}
|
|
2098
2225
|
const pendingBeforeOpen = ctx.appState.pendingPlan(applicationId, branch);
|
|
2099
2226
|
if (planApprovalChanged(approvedPlan, pendingBeforeOpen)) {
|
|
2100
2227
|
return stalePlanApproval(applicationId);
|
|
@@ -2157,7 +2284,7 @@ async function promptExistingApplication(ctx, args) {
|
|
|
2157
2284
|
applicationId,
|
|
2158
2285
|
...(branch ? { branch } : {}),
|
|
2159
2286
|
editUrl: editUrl(ctx, applicationId, branch),
|
|
2160
|
-
mode,
|
|
2287
|
+
...(answeringToolPermission ? {} : { mode }),
|
|
2161
2288
|
status: "building",
|
|
2162
2289
|
}),
|
|
2163
2290
|
};
|
|
@@ -2201,25 +2328,12 @@ async function promptExistingApplication(ctx, args) {
|
|
|
2201
2328
|
type: "progress",
|
|
2202
2329
|
});
|
|
2203
2330
|
const pending = ctx.appState.pendingPlan(applicationId, branch);
|
|
2331
|
+
const mismatchAfterOpen = permissionMismatch();
|
|
2332
|
+
if (mismatchAfterOpen) {
|
|
2333
|
+
return mismatchAfterOpen;
|
|
2334
|
+
}
|
|
2204
2335
|
if (planApprovalChanged(approvedPlan, pending)) {
|
|
2205
|
-
|
|
2206
|
-
if (args.pendingStartToken !== undefined) {
|
|
2207
|
-
const currentDecision = ctx.appState.pendingDecision(applicationId, branch);
|
|
2208
|
-
rememberFailedStart(ctx, {
|
|
2209
|
-
applicationId,
|
|
2210
|
-
branch,
|
|
2211
|
-
...(currentDecision ? { decision: currentDecision.decision } : {}),
|
|
2212
|
-
error: new Error(stale.message),
|
|
2213
|
-
generationStatus: "needs_decision",
|
|
2214
|
-
idempotencyKey: input.idempotencyKey,
|
|
2215
|
-
pendingStartRequired: true,
|
|
2216
|
-
pendingStartToken: args.pendingStartToken,
|
|
2217
|
-
principal,
|
|
2218
|
-
reason: stale.message,
|
|
2219
|
-
revision,
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
return stale;
|
|
2336
|
+
return stalePlanApproval(applicationId);
|
|
2223
2337
|
}
|
|
2224
2338
|
// Approve / refine a pending plan the way the editor's "Build it" / chat
|
|
2225
2339
|
// refine path does. A bare prompt while a plan is pending is refine
|
|
@@ -2253,6 +2367,12 @@ async function promptExistingApplication(ctx, args) {
|
|
|
2253
2367
|
revision,
|
|
2254
2368
|
userId: principal.userId,
|
|
2255
2369
|
};
|
|
2370
|
+
if (permissionReply && pendingInteractive?.kind === "tool_permission") {
|
|
2371
|
+
promptArgs = {
|
|
2372
|
+
...promptArgs,
|
|
2373
|
+
responseMetadata: permissionReply.responseMetadata(pendingInteractive.messageId),
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2256
2376
|
if (effectiveAction === "approve") {
|
|
2257
2377
|
if (pending?.plan) {
|
|
2258
2378
|
promptArgs = {
|
|
@@ -2341,7 +2461,7 @@ async function promptExistingApplication(ctx, args) {
|
|
|
2341
2461
|
applicationId,
|
|
2342
2462
|
...(branch ? { branch } : {}),
|
|
2343
2463
|
editUrl: editUrl(ctx, applicationId, branch),
|
|
2344
|
-
mode: promptArgs.mode,
|
|
2464
|
+
...(answeringToolPermission ? {} : { mode: promptArgs.mode }),
|
|
2345
2465
|
status: "building",
|
|
2346
2466
|
}),
|
|
2347
2467
|
};
|
|
@@ -2384,7 +2504,7 @@ async function promptExistingApplication(ctx, args) {
|
|
|
2384
2504
|
...(forward.value.decision ? { decision: forward.value.decision } : {}),
|
|
2385
2505
|
editUrl: editUrl(ctx, applicationId, branch),
|
|
2386
2506
|
generationStatus: forward.value.generationStatus,
|
|
2387
|
-
mode: promptArgs.mode,
|
|
2507
|
+
...(answeringToolPermission ? {} : { mode: promptArgs.mode }),
|
|
2388
2508
|
...(forward.value.recentRuns && forward.value.recentRuns.length > 0
|
|
2389
2509
|
? { recentRuns: forward.value.recentRuns }
|
|
2390
2510
|
: {}),
|
|
@@ -2687,9 +2807,12 @@ async function applicationName(ctx, principal, applicationId) {
|
|
|
2687
2807
|
return undefined;
|
|
2688
2808
|
}
|
|
2689
2809
|
}
|
|
2690
|
-
function
|
|
2691
|
-
|
|
2692
|
-
|
|
2810
|
+
function pendingInteractiveDecision(ctx, applicationId, branch) {
|
|
2811
|
+
const pending = ctx.appState.pendingDecision(applicationId, branch)?.decision;
|
|
2812
|
+
if (pending?.kind === "multi_choice" || pending?.kind === "tool_permission") {
|
|
2813
|
+
return pending;
|
|
2814
|
+
}
|
|
2815
|
+
return undefined;
|
|
2693
2816
|
}
|
|
2694
2817
|
function editTurnMode(input, pendingQuestion) {
|
|
2695
2818
|
if (pendingQuestion) {
|
|
@@ -2710,6 +2833,66 @@ function awaitingDecision(ctx, scope) {
|
|
|
2710
2833
|
const turn = ctx.sessionPeer.turnState(scope);
|
|
2711
2834
|
return (turn?.phase === "finished" && turn.generationStatus === "needs_decision");
|
|
2712
2835
|
}
|
|
2836
|
+
/**
|
|
2837
|
+
* How a finished Plan turn that changed nothing and owes the host no other step
|
|
2838
|
+
* reads on both `check_app_progress` and `get_app`, or undefined for any other
|
|
2839
|
+
* turn.
|
|
2840
|
+
*
|
|
2841
|
+
* Plan turns cannot write, so one that answered has nothing to retry: its reply
|
|
2842
|
+
* is the answer. One that ended on status labels, reasoning, or silence
|
|
2843
|
+
* answered nothing, so it offers no reply to relay and asks to be run again. A
|
|
2844
|
+
* turn that carries a decision, such as integrations to create, or a failed
|
|
2845
|
+
* checkpoint keeps the step that decision or failure calls for.
|
|
2846
|
+
*/
|
|
2847
|
+
function planQuestionOutcome(turn) {
|
|
2848
|
+
if (turn.generationStatus !== "no_changes" ||
|
|
2849
|
+
turn.mode !== "PLAN" ||
|
|
2850
|
+
turn.decision ||
|
|
2851
|
+
turn.checkpointFailure) {
|
|
2852
|
+
return undefined;
|
|
2853
|
+
}
|
|
2854
|
+
if (!turn.answered) {
|
|
2855
|
+
return { pendingAction: turn.retryAction ?? "retry_build" };
|
|
2856
|
+
}
|
|
2857
|
+
return turn.reply ? { reply: turn.reply } : {};
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* What a turn that wrote nothing asks the host to do next, read the same way
|
|
2861
|
+
* by `check_app_progress` and `get_app`, or undefined for any other turn.
|
|
2862
|
+
*
|
|
2863
|
+
* A turn a newer prompt replaced is never sent again: that would replace the
|
|
2864
|
+
* newer turn in turn. Otherwise an explicit retry wins, a turn that cannot be
|
|
2865
|
+
* retried offers its recovery, if any, a start that never reached Superblocks
|
|
2866
|
+
* offers only the retry it names, and a turn that has nothing to send again
|
|
2867
|
+
* offers no retry (see {@link settledWithoutRetry}). Every other empty turn is
|
|
2868
|
+
* sent again.
|
|
2869
|
+
*/
|
|
2870
|
+
function emptyTurnRecovery(turn) {
|
|
2871
|
+
if (turn.checkpointCommitId || !emptyTurnStatus(turn.generationStatus)) {
|
|
2872
|
+
return undefined;
|
|
2873
|
+
}
|
|
2874
|
+
if (turn.replaced) {
|
|
2875
|
+
return {};
|
|
2876
|
+
}
|
|
2877
|
+
if (turn.retryAction) {
|
|
2878
|
+
return { pendingAction: turn.retryAction };
|
|
2879
|
+
}
|
|
2880
|
+
if (turn.retryable === false) {
|
|
2881
|
+
return turn.recoveryAction ? { pendingAction: turn.recoveryAction } : {};
|
|
2882
|
+
}
|
|
2883
|
+
return turn.startFailed || settledWithoutRetry(turn)
|
|
2884
|
+
? {}
|
|
2885
|
+
: { pendingAction: "retry_build" };
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Whether sending an empty turn's prompt again would do no good: the turn may
|
|
2889
|
+
* still be running elsewhere (detached), or it answered in text without
|
|
2890
|
+
* writing, so the same prompt would most likely get the same answer.
|
|
2891
|
+
*/
|
|
2892
|
+
function settledWithoutRetry(turn) {
|
|
2893
|
+
return (turn.generationStatus === "detached" ||
|
|
2894
|
+
(turn.answered === true && turn.generationStatus === "no_changes"));
|
|
2895
|
+
}
|
|
2713
2896
|
function emptyTurnStatus(status) {
|
|
2714
2897
|
switch (status) {
|
|
2715
2898
|
case "cancelled":
|
|
@@ -2755,16 +2938,10 @@ function localUnbuiltReason(turn, pending) {
|
|
|
2755
2938
|
return status
|
|
2756
2939
|
? {
|
|
2757
2940
|
kind: "empty_turn",
|
|
2758
|
-
...(turn
|
|
2759
|
-
|
|
2760
|
-
: turn.
|
|
2761
|
-
|
|
2762
|
-
? { pendingAction: turn.recoveryAction }
|
|
2763
|
-
: {}
|
|
2764
|
-
: turn.generationStatus === "detached"
|
|
2765
|
-
? {}
|
|
2766
|
-
: { pendingAction: "retry_build" }),
|
|
2767
|
-
...(turn.reply ? { reply: turn.reply } : {}),
|
|
2941
|
+
...(planQuestionOutcome(turn) ?? {
|
|
2942
|
+
...emptyTurnRecovery(turn),
|
|
2943
|
+
...(turn.reply ? { reply: turn.reply } : {}),
|
|
2944
|
+
}),
|
|
2768
2945
|
status,
|
|
2769
2946
|
}
|
|
2770
2947
|
: undefined;
|
|
@@ -2823,52 +3000,64 @@ async function unbuiltApp(ctx, principal, applicationId, requestedBranch) {
|
|
|
2823
3000
|
editUrl: editUrl(ctx, applicationId, actualBranch),
|
|
2824
3001
|
...(name ? { name } : {}),
|
|
2825
3002
|
});
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3003
|
+
return {
|
|
3004
|
+
kind: "ok",
|
|
3005
|
+
value: {
|
|
3006
|
+
...app,
|
|
3007
|
+
...unbuiltStatus(ctx, applicationId, actualBranch, reason),
|
|
3008
|
+
},
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* How an app that cannot be previewed reads as a status. `get_app` reports
|
|
3013
|
+
* this, and so does a `check_app_progress` that has no turn to follow, so the
|
|
3014
|
+
* two reads cannot disagree about the same editor state.
|
|
3015
|
+
*/
|
|
3016
|
+
function unbuiltStatus(ctx, applicationId, branch, reason) {
|
|
3017
|
+
switch (reason.kind) {
|
|
3018
|
+
case "awaiting_decision": {
|
|
3019
|
+
// The remembered plan counts: a plan the caller can show the user beats
|
|
3020
|
+
// sending them to the editor, and it can only be a plan, so reading it
|
|
3021
|
+
// back cannot mislabel a question as one (ENG-5910).
|
|
3022
|
+
const decision = decisionToPutToTheUser(ctx, applicationId, branch, reason.decision);
|
|
3023
|
+
return {
|
|
2835
3024
|
...(decision ? { decision } : {}),
|
|
2836
3025
|
pendingAction: pendingActionForDecision(decision),
|
|
2837
3026
|
status: "needs_decision",
|
|
2838
|
-
}
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
return {
|
|
2843
|
-
kind: "ok",
|
|
2844
|
-
value: {
|
|
2845
|
-
...app,
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
case "checkpoint_failed":
|
|
3030
|
+
return {
|
|
2846
3031
|
checkpointFailure: reason.failure,
|
|
2847
3032
|
pendingAction: "open_editor",
|
|
2848
3033
|
status: "checkpoint_failed",
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
if (reason.kind === "empty_turn") {
|
|
2853
|
-
return {
|
|
2854
|
-
kind: "ok",
|
|
2855
|
-
value: {
|
|
2856
|
-
...app,
|
|
3034
|
+
};
|
|
3035
|
+
case "empty_turn":
|
|
3036
|
+
return {
|
|
2857
3037
|
...(reason.pendingAction
|
|
2858
3038
|
? { pendingAction: reason.pendingAction }
|
|
2859
3039
|
: {}),
|
|
2860
3040
|
...(reason.reply ? { reply: reason.reply } : {}),
|
|
2861
3041
|
status: reason.status,
|
|
2862
|
-
}
|
|
2863
|
-
|
|
3042
|
+
};
|
|
3043
|
+
case "running":
|
|
3044
|
+
return { pendingAction: "wait_for_build", status: "building" };
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
/**
|
|
3048
|
+
* What `check_app_progress` reports when this process has no turn to follow:
|
|
3049
|
+
* the editor state the read just refreshed, read as `get_app` reads it. An app
|
|
3050
|
+
* with nothing to follow and nothing unbuilt is ready.
|
|
3051
|
+
*/
|
|
3052
|
+
async function untrackedStatus(ctx, principal, applicationId, requestedBranch) {
|
|
3053
|
+
const state = await unbuiltReason(ctx, principal, applicationId, requestedBranch);
|
|
3054
|
+
if (!state) {
|
|
3055
|
+
return { status: "ready" };
|
|
2864
3056
|
}
|
|
3057
|
+
const branch = state.branch ?? requestedBranch;
|
|
2865
3058
|
return {
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
...app,
|
|
2869
|
-
pendingAction: "wait_for_build",
|
|
2870
|
-
status: "building",
|
|
2871
|
-
},
|
|
3059
|
+
...(branch ? { branch } : {}),
|
|
3060
|
+
...unbuiltStatus(ctx, applicationId, branch, state.reason),
|
|
2872
3061
|
};
|
|
2873
3062
|
}
|
|
2874
3063
|
/**
|
|
@@ -2903,6 +3092,7 @@ function decisionToPutToTheUser(ctx, applicationId, branch, decision) {
|
|
|
2903
3092
|
* this on the poll loop would flash a blank form through a whole build.
|
|
2904
3093
|
*/
|
|
2905
3094
|
export async function askUser(ctx, input) {
|
|
3095
|
+
const hostDeadlineAtMs = hostCallDeadlineAtMs(ctx);
|
|
2906
3096
|
const principalResult = await ensurePrincipal(ctx);
|
|
2907
3097
|
if (principalResult.kind !== "ok") {
|
|
2908
3098
|
return principalResult;
|
|
@@ -2913,6 +3103,10 @@ export async function askUser(ctx, input) {
|
|
|
2913
3103
|
return resolved;
|
|
2914
3104
|
}
|
|
2915
3105
|
const applicationId = resolved.value;
|
|
3106
|
+
const refreshed = await ensureLiveState(ctx, principal, applicationId, currentTurnBranch(ctx, principal, applicationId, input.branch), hostDeadlineAtMs);
|
|
3107
|
+
if (refreshed.kind !== "ok") {
|
|
3108
|
+
return refreshed;
|
|
3109
|
+
}
|
|
2916
3110
|
const unbuilt = await unbuiltApp(ctx, principal, applicationId, input.branch);
|
|
2917
3111
|
const branch = unbuilt?.value.branch ??
|
|
2918
3112
|
currentTurnBranch(ctx, principal, applicationId, input.branch);
|
|
@@ -2954,6 +3148,7 @@ export async function askUser(ctx, input) {
|
|
|
2954
3148
|
* Read the app's current live editor view without committing or building it.
|
|
2955
3149
|
*/
|
|
2956
3150
|
export async function getApp(ctx, input) {
|
|
3151
|
+
const hostDeadlineAtMs = hostCallDeadlineAtMs(ctx);
|
|
2957
3152
|
const principalResult = await ensurePrincipal(ctx);
|
|
2958
3153
|
if (principalResult.kind !== "ok") {
|
|
2959
3154
|
return principalResult;
|
|
@@ -2965,31 +3160,38 @@ export async function getApp(ctx, input) {
|
|
|
2965
3160
|
}
|
|
2966
3161
|
const applicationId = resolved.value;
|
|
2967
3162
|
const includeScreenshot = input.includeScreenshot !== false;
|
|
3163
|
+
const refreshed = await ensureLiveState(ctx, principal, applicationId, currentTurnBranch(ctx, principal, applicationId, input.branch), hostDeadlineAtMs);
|
|
3164
|
+
if (refreshed.kind !== "ok") {
|
|
3165
|
+
return refreshed;
|
|
3166
|
+
}
|
|
2968
3167
|
const unbuilt = await unbuiltApp(ctx, principal, applicationId, input.branch);
|
|
2969
3168
|
const branch = unbuilt?.value.branch ??
|
|
2970
3169
|
currentTurnBranch(ctx, principal, applicationId, input.branch);
|
|
2971
3170
|
const mayCapture = !unbuilt || unbuilt.value.status === "building";
|
|
2972
3171
|
const live = includeScreenshot && mayCapture
|
|
2973
|
-
? await
|
|
3172
|
+
? await libraryCaptureInput(ctx, applicationId, branch)
|
|
2974
3173
|
: undefined;
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
3174
|
+
if (includeScreenshot && mayCapture && !live) {
|
|
3175
|
+
console.error("gateway live canvas skipped: no_live_context", {
|
|
3176
|
+
applicationId,
|
|
3177
|
+
held: heldSocketFor(ctx, applicationId, branch),
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
if (unbuilt) {
|
|
2979
3181
|
// Plan, question, and empty-starter stays fail-closed: photographing the
|
|
2980
3182
|
// untouched template is the bug unbuiltApp exists to prevent. A running
|
|
2981
3183
|
// turn is different: the held canvas is the thing being written, so a
|
|
2982
3184
|
// live shot is worth returning alongside wait_for_build.
|
|
2983
|
-
if (
|
|
3185
|
+
if (unbuilt.value.status !== "building" ||
|
|
2984
3186
|
!live ||
|
|
2985
|
-
(
|
|
2986
|
-
return
|
|
3187
|
+
(unbuilt.value.branch ?? normalizeBranch(input.branch)) !== branch) {
|
|
3188
|
+
return unbuilt;
|
|
2987
3189
|
}
|
|
2988
3190
|
const liveShot = await captureLiveLibraryIfNeeded(ctx, applicationId, live);
|
|
2989
3191
|
return {
|
|
2990
3192
|
kind: "ok",
|
|
2991
3193
|
value: {
|
|
2992
|
-
...
|
|
3194
|
+
...unbuilt.value,
|
|
2993
3195
|
...(liveShot.needsBrowserDriver ? { needsBrowserDriver: true } : {}),
|
|
2994
3196
|
...(liveShot.screenshotBase64
|
|
2995
3197
|
? { screenshotBase64: liveShot.screenshotBase64 }
|