@loadmill/droid-cua 2.28.0 → 2.30.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 +5 -5
- package/build/index.js +142 -10
- package/build/src/browser/cloud-browser-catalog.js +27 -0
- package/build/src/browser/factory.js +33 -10
- package/build/src/browser/gesture-dispatch.js +281 -0
- package/build/src/browser/interface.js +3 -0
- package/build/src/browser/normalize.js +3 -1
- package/build/src/browser/playwright/actions.js +5 -271
- package/build/src/browser/webdriver/actions.js +10 -0
- package/build/src/browser/webdriver/client.js +83 -0
- package/build/src/browser/webdriver/connection.js +300 -0
- package/build/src/browser/webdriver/page-facade.js +123 -0
- package/build/src/browser/webdriver/session-state.js +35 -0
- package/build/src/cli/headless-execution-config.js +1 -1
- package/build/src/cli/headless-runner.js +26 -13
- package/build/src/cli/headless-target.js +87 -16
- package/build/src/cli/headless-test-source-label.js +4 -0
- package/build/src/cli/loadmill-cloud-device-connect-summary.js +6 -0
- package/build/src/cli/loadmill-cloud-session.js +26 -0
- package/build/src/core/context-optimization.js +1 -1
- package/build/src/core/execution-engine.js +9 -0
- package/build/src/core/report/render-html-report.js +106 -12
- package/build/src/core/report/run-report-sections.js +136 -0
- package/build/src/core/runtime-config.js +2 -2
- package/build/src/desktop-shared/connection-predicates.js +52 -0
- package/build/src/device/cloud/loadmill-cloud/adapter.js +17 -12
- package/build/src/device/cloud/loadmill-cloud/artifact-downloader.js +13 -7
- package/build/src/device/cloud/loadmill-cloud/control-plane-client.js +22 -5
- package/build/src/device/cloud/loadmill-cloud/errors.js +25 -0
- package/build/src/device/cloud/loadmill-cloud/run-pool.js +41 -0
- package/build/src/device/cloud/loadmill-cloud/run-status.js +4 -0
- package/package.json +1 -1
- package/skill/droid-cua/SKILL.md +10 -3
- package/skill/droid-cua/agents/openai.yaml +2 -2
- package/skill/droid-cua/references/authoring-tests.md +36 -6
- package/skill/droid-cua/references/iterative-improvement.md +143 -0
- package/skill/droid-cua/references/onboarding-and-troubleshooting.md +19 -0
- package/skill/droid-cua/references/project-and-auth.md +2 -0
- package/skill/droid-cua/references/running-and-healing.md +32 -14
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLOUD_BROWSER_FAMILIES, CLOUD_BROWSER_VERSION_ALIASES, LOCAL_BROWSER_FAMILIES, } from "../browser/cloud-browser-catalog.js";
|
|
1
2
|
function normalizeTarget(value) {
|
|
2
3
|
if (value == null || value === "") {
|
|
3
4
|
return "mobile";
|
|
@@ -24,18 +25,31 @@ function normalizeSessionMode(value) {
|
|
|
24
25
|
}
|
|
25
26
|
throw new Error("--session-mode must be one of: persistent, fresh.");
|
|
26
27
|
}
|
|
27
|
-
function normalizeBrowser(value) {
|
|
28
|
+
function normalizeBrowser(value, allowedFamilies) {
|
|
28
29
|
if (value == null || value === "") {
|
|
29
30
|
return "chrome";
|
|
30
31
|
}
|
|
31
32
|
if (typeof value !== "string") {
|
|
32
|
-
throw new Error(
|
|
33
|
+
throw new Error(`--browser must be one of: ${allowedFamilies.join(", ")}.`);
|
|
33
34
|
}
|
|
34
35
|
const normalized = value.trim().toLowerCase();
|
|
35
|
-
if (normalized
|
|
36
|
+
if (allowedFamilies.includes(normalized)) {
|
|
36
37
|
return normalized;
|
|
37
38
|
}
|
|
38
|
-
throw new Error(
|
|
39
|
+
throw new Error(`--browser must be one of: ${allowedFamilies.join(", ")}.`);
|
|
40
|
+
}
|
|
41
|
+
function normalizeCloudBrowserVersion(value) {
|
|
42
|
+
if (value == null || value === "") {
|
|
43
|
+
return "latest";
|
|
44
|
+
}
|
|
45
|
+
if (typeof value !== "string") {
|
|
46
|
+
throw new Error(`--browser-version must be one of: ${CLOUD_BROWSER_VERSION_ALIASES.join(", ")}.`);
|
|
47
|
+
}
|
|
48
|
+
const normalized = value.trim().toLowerCase();
|
|
49
|
+
if (CLOUD_BROWSER_VERSION_ALIASES.includes(normalized)) {
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`--browser-version must be one of: ${CLOUD_BROWSER_VERSION_ALIASES.join(", ")}.`);
|
|
39
53
|
}
|
|
40
54
|
const CLOUD_SOURCE_DISPLAY_NAMES = {
|
|
41
55
|
lambdatest: "LambdaTest",
|
|
@@ -80,25 +94,48 @@ function readNonEmptyCliValue(value) {
|
|
|
80
94
|
}
|
|
81
95
|
return null;
|
|
82
96
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
97
|
+
export const MOBILE_ARTIFACT_KINDS = ["video", "deviceLog", "appiumLog"];
|
|
98
|
+
export const WEB_ARTIFACT_KINDS = ["video", "seleniumLog"];
|
|
99
|
+
const UNRESOLVED_POOL_ARTIFACT_KINDS = [...MOBILE_ARTIFACT_KINDS, "seleniumLog"];
|
|
100
|
+
const ARTIFACT_KINDS_BY_POOL = {
|
|
101
|
+
mobile: MOBILE_ARTIFACT_KINDS,
|
|
102
|
+
web: WEB_ARTIFACT_KINDS,
|
|
103
|
+
};
|
|
104
|
+
export function artifactKindsForPool(pool) {
|
|
105
|
+
const kinds = ARTIFACT_KINDS_BY_POOL[pool];
|
|
106
|
+
if (!kinds) {
|
|
107
|
+
throw new Error(`Unknown Loadmill Cloud pool: ${pool}. Expected "mobile" or "web".`);
|
|
108
|
+
}
|
|
109
|
+
return [...kinds];
|
|
110
|
+
}
|
|
111
|
+
export function scopeArtifactKindsToPool(kinds, pool) {
|
|
112
|
+
const allowed = artifactKindsForPool(pool);
|
|
113
|
+
return kinds.filter((kind) => allowed.includes(kind));
|
|
114
|
+
}
|
|
115
|
+
function allowedArtifactKindsForTarget(target, poolKnown) {
|
|
116
|
+
if (!poolKnown) {
|
|
117
|
+
return UNRESOLVED_POOL_ARTIFACT_KINDS;
|
|
118
|
+
}
|
|
119
|
+
return target === "web" ? WEB_ARTIFACT_KINDS : MOBILE_ARTIFACT_KINDS;
|
|
120
|
+
}
|
|
121
|
+
function normalizeArtifactKinds(value, allowedKinds) {
|
|
85
122
|
if (value == null || value === "") {
|
|
86
123
|
return null;
|
|
87
124
|
}
|
|
88
125
|
if (typeof value !== "string") {
|
|
89
|
-
throw new Error(`--artifacts must be a comma-separated list of: ${
|
|
126
|
+
throw new Error(`--artifacts must be a comma-separated list of: ${allowedKinds.join(", ")}, or all.`);
|
|
90
127
|
}
|
|
91
128
|
const raw = value.trim();
|
|
92
129
|
if (!raw) {
|
|
93
130
|
return null;
|
|
94
131
|
}
|
|
95
132
|
if (raw.toLowerCase() === "all") {
|
|
96
|
-
return [...
|
|
133
|
+
return [...allowedKinds];
|
|
97
134
|
}
|
|
98
135
|
const kinds = [...new Set(raw.split(",").map((kind) => kind.trim()).filter(Boolean))];
|
|
99
|
-
const invalid = kinds.filter((kind) => !
|
|
136
|
+
const invalid = kinds.filter((kind) => !allowedKinds.includes(kind));
|
|
100
137
|
if (invalid.length > 0) {
|
|
101
|
-
throw new Error(`--artifacts has invalid kind(s): ${invalid.join(", ")}. Must be one of: ${
|
|
138
|
+
throw new Error(`--artifacts has invalid kind(s): ${invalid.join(", ")}. Must be one of: ${allowedKinds.join(", ")}, or all.`);
|
|
102
139
|
}
|
|
103
140
|
return kinds;
|
|
104
141
|
}
|
|
@@ -144,9 +181,10 @@ function normalizeCloudAppPath(value, platform, deviceSource) {
|
|
|
144
181
|
export function shouldWaitForLoadmillCloudCompletion(targetConfig) {
|
|
145
182
|
return Array.isArray(targetConfig?.artifacts) && targetConfig.artifacts.length > 0;
|
|
146
183
|
}
|
|
184
|
+
const MOBILE_ONLY_CLOUD_SOURCES = new Set(["lambdatest", "aws-device-farm"]);
|
|
147
185
|
export function resolveHeadlessTarget(args = {}) {
|
|
148
186
|
const target = normalizeTarget(args.target);
|
|
149
|
-
const
|
|
187
|
+
const targetGiven = hasNonEmptyString(args.target);
|
|
150
188
|
const avdName = hasNonEmptyString(args.avd) ? args.avd.trim() : null;
|
|
151
189
|
const platform = normalizeMobilePlatform(args.platform, "--platform");
|
|
152
190
|
// The mtc_ prefix on --cloud-session-id is loadmill-cloud's; when no
|
|
@@ -164,14 +202,44 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
164
202
|
if (avdName || platform) {
|
|
165
203
|
throw new Error("--avd and --platform cannot be used with --target web.");
|
|
166
204
|
}
|
|
167
|
-
if (deviceSource
|
|
168
|
-
throw new Error(
|
|
205
|
+
if (MOBILE_ONLY_CLOUD_SOURCES.has(deviceSource)) {
|
|
206
|
+
throw new Error(`--device-source ${deviceSource} can only be used with --target mobile.`);
|
|
207
|
+
}
|
|
208
|
+
if (hasNonEmptyString(args["device-name"])) {
|
|
209
|
+
throw new Error("--device-name cannot be used with --target web.");
|
|
210
|
+
}
|
|
211
|
+
if (readNonEmptyCliValue(args["os-version"]) != null) {
|
|
212
|
+
throw new Error("--os-version cannot be used with --target web.");
|
|
213
|
+
}
|
|
214
|
+
if (hasNonEmptyString(args.app)) {
|
|
215
|
+
throw new Error("--app cannot be used with --target web.");
|
|
216
|
+
}
|
|
217
|
+
if (deviceSource === "loadmill-cloud") {
|
|
218
|
+
if (args["session-mode"] != null) {
|
|
219
|
+
throw new Error("--session-mode can only be used with --device-source local.");
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
target,
|
|
223
|
+
sessionMode: null,
|
|
224
|
+
browserFamily: normalizeBrowser(args.browser, CLOUD_BROWSER_FAMILIES),
|
|
225
|
+
browserVersion: normalizeCloudBrowserVersion(args["browser-version"]),
|
|
226
|
+
deviceSource: "loadmill-cloud",
|
|
227
|
+
avdName: null,
|
|
228
|
+
platform: null,
|
|
229
|
+
deviceName: null,
|
|
230
|
+
osVersion: null,
|
|
231
|
+
appPath: null,
|
|
232
|
+
cloudSessionId: readNonEmptyCliValue(args["cloud-session-id"]),
|
|
233
|
+
artifacts: normalizeArtifactKinds(args.artifacts, WEB_ARTIFACT_KINDS),
|
|
234
|
+
listDevices: false,
|
|
235
|
+
};
|
|
169
236
|
}
|
|
170
237
|
assertNoCloudFlags(args);
|
|
171
238
|
return {
|
|
172
239
|
target,
|
|
173
|
-
sessionMode,
|
|
174
|
-
browserFamily: normalizeBrowser(args.browser),
|
|
240
|
+
sessionMode: normalizeSessionMode(args["session-mode"]),
|
|
241
|
+
browserFamily: normalizeBrowser(args.browser, LOCAL_BROWSER_FAMILIES),
|
|
242
|
+
browserVersion: null,
|
|
175
243
|
deviceSource: "local",
|
|
176
244
|
avdName: null,
|
|
177
245
|
platform: null,
|
|
@@ -195,6 +263,7 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
195
263
|
target,
|
|
196
264
|
sessionMode: null,
|
|
197
265
|
browserFamily: null,
|
|
266
|
+
browserVersion: null,
|
|
198
267
|
deviceSource,
|
|
199
268
|
avdName: null,
|
|
200
269
|
platform: null,
|
|
@@ -215,7 +284,7 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
215
284
|
const cloudSessionId = readNonEmptyCliValue(args["cloud-session-id"]);
|
|
216
285
|
const isReconnect = deviceSource === "loadmill-cloud" && Boolean(cloudSessionId);
|
|
217
286
|
const appPath = normalizeCloudAppPath(args.app, platform, deviceSource);
|
|
218
|
-
const artifacts = normalizeArtifactKinds(args.artifacts);
|
|
287
|
+
const artifacts = normalizeArtifactKinds(args.artifacts, allowedArtifactKindsForTarget(target, targetGiven || !isReconnect));
|
|
219
288
|
if (artifacts && deviceSource !== "loadmill-cloud") {
|
|
220
289
|
throw new Error("--artifacts can only be used with --device-source loadmill-cloud.");
|
|
221
290
|
}
|
|
@@ -241,6 +310,7 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
241
310
|
target,
|
|
242
311
|
sessionMode: null,
|
|
243
312
|
browserFamily: null,
|
|
313
|
+
browserVersion: null,
|
|
244
314
|
deviceSource,
|
|
245
315
|
avdName: null,
|
|
246
316
|
platform,
|
|
@@ -256,6 +326,7 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
256
326
|
target,
|
|
257
327
|
sessionMode: null,
|
|
258
328
|
browserFamily: null,
|
|
329
|
+
browserVersion: null,
|
|
259
330
|
deviceSource,
|
|
260
331
|
avdName,
|
|
261
332
|
platform,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export async function withLoadmillCloudSession({ credentials, connect, teardown, run = null }) {
|
|
2
|
+
const targetId = await connect(credentials);
|
|
3
|
+
if (typeof run !== "function") {
|
|
4
|
+
return targetId;
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
return await run(targetId);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
await teardown();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function createBrowserSessionTeardown({ disconnect, releaseSession }) {
|
|
14
|
+
return async () => {
|
|
15
|
+
try {
|
|
16
|
+
await disconnect();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
console.error(`[loadmill-cloud] browser disconnect failed: ${message}`);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await releaseSession();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const DEFAULT_CONTEXT_OPTIMIZATION_ENABLED = true;
|
|
2
|
-
export const DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD =
|
|
2
|
+
export const DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD = 60000;
|
|
3
3
|
export const MIN_CONTEXT_OPTIMIZATION_THRESHOLD = 10000;
|
|
4
4
|
export const MAX_CONTEXT_OPTIMIZATION_THRESHOLD = 900000;
|
|
5
5
|
export const CONTEXT_OPTIMIZATION_ENABLED_ENV = "DROID_CUA_CONTEXT_OPTIMIZATION_ENABLED";
|
|
@@ -503,6 +503,15 @@ export class ExecutionEngine {
|
|
|
503
503
|
}
|
|
504
504
|
emitResponseOutputItems(items, addOutput, eventMeta, options = {}) {
|
|
505
505
|
for (const item of items) {
|
|
506
|
+
if (item.type === "compaction") {
|
|
507
|
+
addOutput({
|
|
508
|
+
type: "info",
|
|
509
|
+
text: "Optimized context",
|
|
510
|
+
eventType: "system_message",
|
|
511
|
+
...eventMeta({ payload: { event: "context.optimized" } }),
|
|
512
|
+
});
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
506
515
|
if (item.type === "reasoning") {
|
|
507
516
|
for (const entry of item.summary) {
|
|
508
517
|
if (entry.type !== "summary_text")
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// no external references — screenshots must already be inlined as data URLs in
|
|
4
4
|
// `event.payload.screenshotDataUrl`. Use generate-html-report.js to inline them.
|
|
5
5
|
import { assertionData, formatDuration, hasNearbyAssertionCard, isAssistantAssertionSummary, isDuplicateAssertionLine, isHiddenExecutionLogEvent, runSummaryData, stepCompletionData, stepCompletionReasonTooltip, } from "./run-log-model.js";
|
|
6
|
+
import { buildRunReportSections, capitalizeReportLine, cloudProviderId, cloudProviderLabel, cloudStatus, } from "./run-report-sections.js";
|
|
7
|
+
import { LOADMILL_MARK_PATHS } from "../../brand/loadmill-mark.js";
|
|
6
8
|
// ---------- text / value formatting (mirrors the Desktop App) ----------
|
|
7
9
|
function escapeHtml(value) {
|
|
8
10
|
return String(value ?? "")
|
|
@@ -12,6 +14,22 @@ function escapeHtml(value) {
|
|
|
12
14
|
.replace(/"/g, """)
|
|
13
15
|
.replace(/'/g, "'");
|
|
14
16
|
}
|
|
17
|
+
function renderLinkedText(value) {
|
|
18
|
+
const text = String(value ?? "");
|
|
19
|
+
const pattern = /https?:\/\/[^\s]+/g;
|
|
20
|
+
let cursor = 0;
|
|
21
|
+
let html = "";
|
|
22
|
+
for (const match of text.matchAll(pattern)) {
|
|
23
|
+
const start = match.index ?? 0;
|
|
24
|
+
const raw = match[0];
|
|
25
|
+
const trailing = raw.match(/[).,;:!?]+$/)?.[0] ?? "";
|
|
26
|
+
const url = trailing ? raw.slice(0, -trailing.length) : raw;
|
|
27
|
+
html += escapeHtml(text.slice(cursor, start));
|
|
28
|
+
html += `<a href="${escapeHtml(url)}" target="_blank" rel="noreferrer">${escapeHtml(url)}</a>${escapeHtml(trailing)}`;
|
|
29
|
+
cursor = start + raw.length;
|
|
30
|
+
}
|
|
31
|
+
return html + escapeHtml(text.slice(cursor));
|
|
32
|
+
}
|
|
15
33
|
function formatRunTimestamp(iso) {
|
|
16
34
|
if (!iso)
|
|
17
35
|
return "—";
|
|
@@ -95,10 +113,16 @@ function screenshotImg(event, shots) {
|
|
|
95
113
|
function icon(name, shapes) {
|
|
96
114
|
return `<svg class="dc-icon" data-icon="${name}" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${shapes}</svg>`;
|
|
97
115
|
}
|
|
116
|
+
function loadmillIcon() {
|
|
117
|
+
const paths = LOADMILL_MARK_PATHS.map(({ d, fill }) => `<path d="${d}" fill="${fill}"/>`).join("");
|
|
118
|
+
return `<svg class="dc-icon" data-icon="loadmill-mark" aria-hidden="true" viewBox="0 0 1035 1035">${paths}</svg>`;
|
|
119
|
+
}
|
|
98
120
|
const ICONS = {
|
|
99
121
|
arrowUpDown: icon("arrow-up-down", `<path d="m21 16-4 4-4-4"/><path d="M17 20V4"/><path d="m3 8 4-4 4 4"/><path d="M7 4v16"/>`),
|
|
122
|
+
bookOpen: icon("book-open", `<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>`),
|
|
100
123
|
botMessageSquare: icon("bot-message-square", `<path d="M12 6V2H8"/><path d="m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z"/><path d="M2 12h2"/><path d="M9 11v2"/><path d="M15 11v2"/><path d="M20 12h2"/>`),
|
|
101
124
|
camera: icon("camera", `<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>`),
|
|
125
|
+
cloud: icon("cloud", `<path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/>`),
|
|
102
126
|
compass: icon("compass", `<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/><circle cx="12" cy="12" r="10"/>`),
|
|
103
127
|
flag: icon("flag", `<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/>`),
|
|
104
128
|
keyboard: icon("keyboard", `<path d="M10 8h.01"/><path d="M12 12h.01"/><path d="M14 8h.01"/><path d="M16 12h.01"/><path d="M18 8h.01"/><path d="M6 8h.01"/><path d="M7 16h10"/><path d="M8 12h.01"/><rect width="20" height="16" x="2" y="4" rx="2"/>`),
|
|
@@ -109,6 +133,7 @@ const ICONS = {
|
|
|
109
133
|
rotateCcw: icon("rotate-ccw", `<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>`),
|
|
110
134
|
squareX: icon("square-x", `<rect width="18" height="18" x="3" y="3" rx="2"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>`),
|
|
111
135
|
timer: icon("timer", `<line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/>`),
|
|
136
|
+
loadmill: loadmillIcon(),
|
|
112
137
|
};
|
|
113
138
|
function actionIcon(event) {
|
|
114
139
|
const lowerText = event.text.toLowerCase();
|
|
@@ -137,6 +162,9 @@ function actionIcon(event) {
|
|
|
137
162
|
function renderAgentText(text) {
|
|
138
163
|
return `<div class="dc-line dc-agent-line">${ICONS.botMessageSquare}<span>${escapeHtml(text)}</span></div>`;
|
|
139
164
|
}
|
|
165
|
+
function textIcon(value) {
|
|
166
|
+
return `<span class="dc-icon" aria-hidden="true">${value}</span>`;
|
|
167
|
+
}
|
|
140
168
|
// ---------- per-event timeline rendering (mirrors ExecutionLogItem.tsx) ----------
|
|
141
169
|
function renderEvent(event, shots) {
|
|
142
170
|
if (isHiddenExecutionLogEvent(event))
|
|
@@ -153,13 +181,13 @@ function renderEvent(event, shots) {
|
|
|
153
181
|
: "Test Failed";
|
|
154
182
|
const badge = summary.status === "passed" ? "PASS" : summary.status === "stopped" ? "STOPPED" : summary.status === "errored" ? "ERROR" : "FAIL";
|
|
155
183
|
return `<div class="dc-card dc-${cls}">
|
|
156
|
-
<div class="dc-card-head"><span class="dc-card-title">${escapeHtml(label)}</span><span class="dc-pill dc-pill-${cls}">${escapeHtml(badge)}</span></div>
|
|
184
|
+
<div class="dc-card-head"><span class="dc-card-title">${textIcon(summary.status === "passed" ? "✓" : "!")}${escapeHtml(label)}</span><span class="dc-pill dc-pill-${cls}">${escapeHtml(badge)}</span></div>
|
|
157
185
|
<div class="dc-card-body">
|
|
158
186
|
<div>Duration: ${escapeHtml(formatDuration(summary.durationMs))}</div>
|
|
159
187
|
<div>Steps: ${summary.instructionsCompleted ?? 0}/${summary.instructionsTotal ?? 0}</div>
|
|
160
188
|
<div>Assertions: ${summary.assertionsPassed ?? 0}/${(summary.assertionsPassed ?? 0) + (summary.assertionsFailed ?? 0)}</div>
|
|
161
189
|
<div>Retries: ${summary.retries ?? 0}</div>
|
|
162
|
-
</div>${summary.error ? `\n <div class="dc-card-error">${
|
|
190
|
+
</div>${summary.error ? `\n <div class="dc-card-error">${renderLinkedText(summary.error)}</div>` : ""}
|
|
163
191
|
</div>`;
|
|
164
192
|
}
|
|
165
193
|
const isInstruction = event.eventType === "instruction_started" || event.kind === "user";
|
|
@@ -186,7 +214,7 @@ function renderEvent(event, shots) {
|
|
|
186
214
|
const cls = assertion.passed ? "pass" : "fail";
|
|
187
215
|
const img = screenshotImg(event, shots);
|
|
188
216
|
return `<div class="dc-card dc-${cls}">
|
|
189
|
-
<div class="dc-card-head"><span class="dc-card-title">${assertion.passed ? "Assertion Passed" : "Assertion Failed"}${img}</span><span class="dc-pill dc-pill-${cls}">${assertion.passed ? "PASS" : "FAIL"}</span></div>
|
|
217
|
+
<div class="dc-card-head"><span class="dc-card-title">${textIcon(assertion.passed ? "✓" : "!")}${assertion.passed ? "Assertion Passed" : "Assertion Failed"}${img}</span><span class="dc-pill dc-pill-${cls}">${assertion.passed ? "PASS" : "FAIL"}</span></div>
|
|
190
218
|
<div class="dc-assertion">${escapeHtml(assertion.assertion)}</div>${assertion.details ? `\n <div class="dc-assertion-details">${escapeHtml(assertion.details)}</div>` : ""}
|
|
191
219
|
</div>`;
|
|
192
220
|
}
|
|
@@ -194,18 +222,64 @@ function renderEvent(event, shots) {
|
|
|
194
222
|
return `<div class="dc-line dc-retry">${ICONS.rotateCcw}<span>${escapeHtml(event.text)}</span></div>`;
|
|
195
223
|
}
|
|
196
224
|
if (event.kind === "action") {
|
|
197
|
-
return `<div class="dc-line">${actionIcon(event)}<span>${escapeHtml(event.text)}</span>${screenshotImg(event, shots)}</div>`;
|
|
225
|
+
return `<div class="dc-line dc-action">${actionIcon(event)}<span>${escapeHtml(event.text)}</span>${screenshotImg(event, shots)}</div>`;
|
|
198
226
|
}
|
|
199
227
|
if (event.eventType === "screenshot_captured") {
|
|
200
228
|
return `<div class="dc-line">${ICONS.camera}<span>${escapeHtml(event.text)}</span>${screenshotImg(event, shots)}</div>`;
|
|
201
229
|
}
|
|
202
|
-
|
|
230
|
+
if (event.eventType === "design_generated_script" || (event.eventType === "tool_call" && event.payload?.toolName === "test_design_complete")) {
|
|
231
|
+
return `<div class="dc-line">${textIcon("✦")}<span>${escapeHtml(event.text)}</span></div>`;
|
|
232
|
+
}
|
|
233
|
+
return `<div class="dc-line dc-${escapeHtml(event.kind)}">${renderLinkedText(event.text)}</div>`;
|
|
203
234
|
}
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
235
|
+
function renderAppContextCard(event) {
|
|
236
|
+
const text = typeof event.payload?.text === "string" ? event.payload.text.trim() : "";
|
|
237
|
+
if (!text)
|
|
238
|
+
return "";
|
|
239
|
+
return `<details class="dc-special dc-context">
|
|
240
|
+
<summary><span class="dc-special-title">${ICONS.bookOpen}App Context</span></summary>
|
|
241
|
+
<pre>${escapeHtml(text)}</pre>
|
|
242
|
+
</details>`;
|
|
243
|
+
}
|
|
244
|
+
function renderCloudCard(events) {
|
|
245
|
+
const label = cloudProviderLabel(events.map(cloudProviderId).find(Boolean));
|
|
246
|
+
const status = cloudStatus(events);
|
|
247
|
+
const lines = events
|
|
248
|
+
.filter((event) => event.payload?.event !== "cloud.app.check" && event.payload?.event !== "cloud.app.reupload_needed")
|
|
249
|
+
.map((event) => capitalizeReportLine(event.text))
|
|
250
|
+
.filter((text, index, all) => text !== all[index - 1]);
|
|
251
|
+
return `<details class="dc-special dc-cloud"${status === "issue" || status === "starting" ? " open" : ""}>
|
|
252
|
+
<summary><span class="dc-special-title">${ICONS.cloud}${escapeHtml(label)}</span><span class="dc-pill dc-pill-${status}">${status}</span></summary>
|
|
253
|
+
<div class="dc-special-body">${lines.map((line) => `<div>${renderLinkedText(line)}</div>`).join("")}</div>
|
|
254
|
+
</details>`;
|
|
255
|
+
}
|
|
256
|
+
function renderLoadmillCard(events) {
|
|
257
|
+
const lines = events.map((event) => event.text.replace(/^\[Loadmill\]\s*/, "").trim());
|
|
258
|
+
const failed = events.some((event) => /\[Loadmill\]\s*FAILED|^Error:/i.test(event.text));
|
|
259
|
+
const passed = !failed && events.some((event) => /\bpassed\b/i.test(event.text));
|
|
260
|
+
const status = failed ? "failed" : passed ? "passed" : "running";
|
|
261
|
+
return `<details class="dc-special dc-loadmill"${passed ? "" : " open"}>
|
|
262
|
+
<summary><span class="dc-special-title">${ICONS.loadmill}Loadmill Flow</span><span class="dc-pill dc-pill-${status}">${status}</span></summary>
|
|
263
|
+
<div class="dc-special-body">${lines.map((line) => `<div>${renderLinkedText(line)}</div>`).join("")}</div>
|
|
264
|
+
</details>`;
|
|
265
|
+
}
|
|
266
|
+
function renderTimeline(events, shots, warnings = []) {
|
|
267
|
+
const html = buildRunReportSections(buildVisibleEvents(events))
|
|
268
|
+
.map((item) => {
|
|
269
|
+
if (item.kind === "app-context")
|
|
270
|
+
return renderAppContextCard(item.line);
|
|
271
|
+
if (item.kind === "cloud")
|
|
272
|
+
return renderCloudCard(item.lines);
|
|
273
|
+
if (item.kind === "loadmill")
|
|
274
|
+
return renderLoadmillCard(item.lines);
|
|
275
|
+
return renderEvent(item.line, shots);
|
|
276
|
+
})
|
|
207
277
|
.filter(Boolean)
|
|
208
278
|
.join("\n");
|
|
279
|
+
if (html)
|
|
280
|
+
return html;
|
|
281
|
+
const message = warnings.find((warning) => warning.toLowerCase().includes("event log not available")) ?? "No event timeline available for this report.";
|
|
282
|
+
return `<div class="dc-empty">${escapeHtml(message)}</div>`;
|
|
209
283
|
}
|
|
210
284
|
// Full-size overlays, one per screenshot. Shown via the CSS `:target` rule, so
|
|
211
285
|
// click-to-enlarge and prev/next/close work with JavaScript disabled.
|
|
@@ -290,7 +364,7 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
290
364
|
.dc-test-row { display: grid; grid-template-columns: 1fr 90px 90px; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 6px; }
|
|
291
365
|
.dc-test-name { font-weight: 500; color: #334155; }
|
|
292
366
|
.dc-test-dur { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: #64748b; font-size: 12px; }
|
|
293
|
-
.dc-timeline { background: #fff; border: 1px solid #cbd5e1; border-radius: 12px; padding:
|
|
367
|
+
.dc-timeline { background: #fff; border: 1px solid #cbd5e1; border-radius: 12px; padding: 20px 24px; display: flex; flex-direction: column; gap: 8px; }
|
|
294
368
|
.dc-child-block { margin-top: 12px; }
|
|
295
369
|
.dc-child-head { font-size: 13px; font-weight: 600; color: #334155; margin: 8px 0; }
|
|
296
370
|
.dc-row { display: flex; }
|
|
@@ -300,6 +374,9 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
300
374
|
.dc-agent-line { align-items: flex-start; }
|
|
301
375
|
.dc-agent-line .dc-icon { margin-top: 4px; }
|
|
302
376
|
.dc-icon { width: 14px; height: 14px; flex: 0 0 auto; color: #94a3b8; }
|
|
377
|
+
.dc-line a, .dc-card-error a, .dc-special-body a { color: inherit; font-weight: 500; text-underline-offset: 2px; }
|
|
378
|
+
.dc-action { position: relative; min-height: 24px; }
|
|
379
|
+
.dc-action + .dc-action::before { content: ""; position: absolute; left: 7px; top: -9px; height: 10px; border-left: 1px solid #cbd5e1; }
|
|
303
380
|
.dc-retry { color: #b45309; }
|
|
304
381
|
.dc-error { color: #b91c1c; }
|
|
305
382
|
.dc-warning { color: #b45309; }
|
|
@@ -318,7 +395,7 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
318
395
|
.dc-lightbox-next { right: 10px; }
|
|
319
396
|
.dc-card { border: 1px solid; border-radius: 12px; padding: 12px 14px; }
|
|
320
397
|
.dc-card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 6px; }
|
|
321
|
-
.dc-card-title { font-size: 13px; font-weight: 600; display: inline-flex; align-items: center; }
|
|
398
|
+
.dc-card-title { font-size: 13px; font-weight: 600; display: inline-flex; align-items: center; gap: 6px; }
|
|
322
399
|
.dc-card-body > div { margin: 1px 0; }
|
|
323
400
|
.dc-card-error, .dc-assertion-details { margin-top: 6px; font-size: 13px; }
|
|
324
401
|
.dc-assertion { font-size: 14px; }
|
|
@@ -331,6 +408,23 @@ body { margin: 0; background: #f1f5f9; color: #0f172a; font-family: -apple-syste
|
|
|
331
408
|
.dc-pill-error, .dc-pill-errored, .dc-pill-amber { border-color: #fcd34d; background: #fef3c7; color: #b45309; }
|
|
332
409
|
.dc-pill-stopped { border-color: #fdba74; background: #ffedd5; color: #c2410c; }
|
|
333
410
|
.dc-pill-skipped { border-color: #cbd5e1; background: #f1f5f9; color: #334155; }
|
|
411
|
+
.dc-pill-ready, .dc-pill-passed { border-color: #6ee7b7; background: #d1fae5; color: #047857; }
|
|
412
|
+
.dc-pill-issue, .dc-pill-failed { border-color: #fda4af; background: #ffe4e6; color: #be123c; }
|
|
413
|
+
.dc-pill-ended { border-color: #cbd5e1; background: #f1f5f9; color: #334155; }
|
|
414
|
+
.dc-pill-starting { border-color: #a5b4fc; background: #e0e7ff; color: #4338ca; }
|
|
415
|
+
.dc-pill-running { border-color: #7dd3fc; background: #e0f2fe; color: #0369a1; }
|
|
416
|
+
.dc-special { width: 100%; border: 1px solid; border-radius: 12px; padding: 11px 14px; }
|
|
417
|
+
.dc-special summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; cursor: pointer; list-style: none; }
|
|
418
|
+
.dc-special summary::-webkit-details-marker { display: none; }
|
|
419
|
+
.dc-special summary::before { content: "›"; margin-right: 4px; color: currentColor; font-size: 18px; line-height: 1; transform-origin: center; }
|
|
420
|
+
.dc-special[open] summary::before { transform: rotate(90deg); }
|
|
421
|
+
.dc-special-title { display: inline-flex; flex: 1; align-items: center; gap: 5px; font-size: 13px; font-weight: 600; }
|
|
422
|
+
.dc-special-body { margin-top: 9px; display: flex; flex-direction: column; gap: 5px; color: #334155; }
|
|
423
|
+
.dc-context { border-color: #ddd6fe; background: #f5f3ff; color: #4c1d95; }
|
|
424
|
+
.dc-context pre { margin: 10px 0 0; border: 1px solid #ede9fe; border-radius: 8px; background: rgba(255,255,255,.75); padding: 8px 10px; color: #334155; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
|
|
425
|
+
.dc-cloud { border-color: #c7d2fe; background: #eef2ff; color: #312e81; }
|
|
426
|
+
.dc-loadmill { border-color: #bae6fd; background: #f0f9ff; color: #075985; }
|
|
427
|
+
.dc-empty { color: #64748b; }
|
|
334
428
|
@keyframes dc-spin { to { transform: rotate(360deg); } }
|
|
335
429
|
`.trim();
|
|
336
430
|
// ---------- entry point ----------
|
|
@@ -343,7 +437,7 @@ export function renderHtmlReport(model, { title, includeScripts = true } = {}) {
|
|
|
343
437
|
let body;
|
|
344
438
|
if (model.childTests && model.childTests.length > 0) {
|
|
345
439
|
const children = model.childTests
|
|
346
|
-
.map((child) => `<div class="dc-child-block"><div class="dc-child-head">${escapeHtml(child.testName.replace(/\.dcua$/i, ""))}</div>${renderTimeline(child.events, shots)}</div>`)
|
|
440
|
+
.map((child) => `<div class="dc-child-block"><div class="dc-child-head">${escapeHtml(child.testName.replace(/\.dcua$/i, ""))}</div>${renderTimeline(child.events, shots, child.warnings)}</div>`)
|
|
347
441
|
.join("\n");
|
|
348
442
|
body = `${renderSummaryCard(model)}
|
|
349
443
|
${renderTestsList(model.childTests)}
|
|
@@ -354,7 +448,7 @@ ${children}
|
|
|
354
448
|
else {
|
|
355
449
|
body = `${renderSummaryCard(model)}
|
|
356
450
|
<div class="dc-timeline">
|
|
357
|
-
${renderTimeline(model.events, shots)}
|
|
451
|
+
${renderTimeline(model.events, shots, model.warnings)}
|
|
358
452
|
</div>`;
|
|
359
453
|
}
|
|
360
454
|
const lightboxes = renderLightboxes(shots);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const CLOUD_PROVIDERS = [
|
|
2
|
+
["browserstack", "BrowserStack", "BrowserStack"],
|
|
3
|
+
["lambdatest", "LambdaTest", "LambdaTest"],
|
|
4
|
+
["aws-device-farm", "(?:AWS )?Device Farm", "AWS"],
|
|
5
|
+
["loadmill-cloud", "Loadmill Cloud", "Loadmill Cloud"],
|
|
6
|
+
];
|
|
7
|
+
export function cloudProviderId(line) {
|
|
8
|
+
const payloadProviderId = line.payload?.providerId;
|
|
9
|
+
const fromPayload = CLOUD_PROVIDERS.find(([id]) => id === payloadProviderId);
|
|
10
|
+
if (fromPayload)
|
|
11
|
+
return fromPayload[0];
|
|
12
|
+
for (const [id, name] of CLOUD_PROVIDERS) {
|
|
13
|
+
if (new RegExp(`^(?:Preparing|Provisioning|Creating|Connecting to|Connected to|Ending|Checking cached|Reused cached|Cached|Reusing uploaded app .* for|Uploading .* to|Local app changed since the last) ${name}\\b`, "i").test(line.text) ||
|
|
14
|
+
new RegExp(`^${name} session (?:started|ended)`, "i").test(line.text)) {
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function cloudProviderLabel(providerId) {
|
|
21
|
+
return CLOUD_PROVIDERS.find(([id]) => id === providerId)?.[2] ?? "Cloud session";
|
|
22
|
+
}
|
|
23
|
+
export function capitalizeReportLine(text) {
|
|
24
|
+
return text.replace(/^(\s*)([a-z])/, (_, whitespace, letter) => whitespace + letter.toUpperCase());
|
|
25
|
+
}
|
|
26
|
+
export function isCloudSessionLine(line) {
|
|
27
|
+
if (cloudProviderId(line))
|
|
28
|
+
return true;
|
|
29
|
+
return /^Upload complete for /i.test(line.text);
|
|
30
|
+
}
|
|
31
|
+
export function isCloudSessionContinuationLine(line) {
|
|
32
|
+
return !cloudProviderId(line) && /^Platform:\s/i.test(line.text);
|
|
33
|
+
}
|
|
34
|
+
export function cloudStatus(lines) {
|
|
35
|
+
if (lines.some((line) => line.kind === "error" || line.kind === "warning"))
|
|
36
|
+
return "issue";
|
|
37
|
+
if (lines.some((line) => line.payload?.event === "cloud.session.ended" || line.payload?.event === "cloud.session.ending"))
|
|
38
|
+
return "ended";
|
|
39
|
+
if (lines.some((line) => line.payload?.event === "cloud.session.connected"))
|
|
40
|
+
return "ready";
|
|
41
|
+
return "starting";
|
|
42
|
+
}
|
|
43
|
+
function isAppContextLine(line) {
|
|
44
|
+
return line.payload?.event === "app_context.loaded";
|
|
45
|
+
}
|
|
46
|
+
function isLoadmillLine(line) {
|
|
47
|
+
return line.text.startsWith("[Loadmill]");
|
|
48
|
+
}
|
|
49
|
+
function isLoadmillContinuationLine(line) {
|
|
50
|
+
return line.text.startsWith(" ") ||
|
|
51
|
+
line.text.startsWith("Command:") ||
|
|
52
|
+
line.text.startsWith("Error:") ||
|
|
53
|
+
line.text.startsWith("Skipping failed Loadmill");
|
|
54
|
+
}
|
|
55
|
+
function loadmillSuiteRunId(line) {
|
|
56
|
+
const payloadRunId = line.payload?.loadmillSuiteRunId;
|
|
57
|
+
if (typeof payloadRunId === "string" && payloadRunId.trim())
|
|
58
|
+
return payloadRunId.trim();
|
|
59
|
+
return line.text.match(/Test started \(ID:\s*([^)]+)\)/i)?.[1]?.trim() ||
|
|
60
|
+
line.text.match(/Run ID:\s*(.+)$/i)?.[1]?.trim() || null;
|
|
61
|
+
}
|
|
62
|
+
function loadmillCommand(line) {
|
|
63
|
+
const payloadCommand = line.payload?.loadmillCommand;
|
|
64
|
+
if (typeof payloadCommand === "string" && payloadCommand.trim())
|
|
65
|
+
return payloadCommand.trim();
|
|
66
|
+
return line.text.match(/^\[Loadmill\]\s*Executing:\s*(.+)$/i)?.[1]?.trim() || null;
|
|
67
|
+
}
|
|
68
|
+
function shouldAppendToPreviousLoadmillBlock(line, previousLines) {
|
|
69
|
+
if (previousLines.some((previous) => /\[Loadmill\]\s*FAILED|^Error:|\bpassed\b/i.test(previous.text)))
|
|
70
|
+
return false;
|
|
71
|
+
const runId = loadmillSuiteRunId(line);
|
|
72
|
+
if (runId)
|
|
73
|
+
return previousLines.some((previous) => loadmillSuiteRunId(previous) === runId);
|
|
74
|
+
const command = loadmillCommand(line);
|
|
75
|
+
return Boolean(command && previousLines.some((previous) => loadmillCommand(previous) === command));
|
|
76
|
+
}
|
|
77
|
+
function findPreviousSection(sections, kind) {
|
|
78
|
+
for (let index = sections.length - 1; index >= 0; index -= 1) {
|
|
79
|
+
if (sections[index].kind === kind)
|
|
80
|
+
return sections[index];
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function findStartingCloudSection(sections, providerId) {
|
|
85
|
+
if (!providerId)
|
|
86
|
+
return null;
|
|
87
|
+
const previous = findPreviousSection(sections, "cloud");
|
|
88
|
+
if (!previous || cloudStatus(previous.lines) !== "starting")
|
|
89
|
+
return null;
|
|
90
|
+
return previous.lines.some((line) => cloudProviderId(line) === providerId) ? previous : null;
|
|
91
|
+
}
|
|
92
|
+
export function buildRunReportSections(logs) {
|
|
93
|
+
const sections = [];
|
|
94
|
+
let index = 0;
|
|
95
|
+
while (index < logs.length) {
|
|
96
|
+
const current = logs[index];
|
|
97
|
+
if (isAppContextLine(current)) {
|
|
98
|
+
sections.push({ kind: "app-context", line: current });
|
|
99
|
+
index += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (isCloudSessionLine(current)) {
|
|
103
|
+
const lines = [current];
|
|
104
|
+
index += 1;
|
|
105
|
+
while (index < logs.length && (isCloudSessionLine(logs[index]) || isCloudSessionContinuationLine(logs[index]))) {
|
|
106
|
+
lines.push(logs[index++]);
|
|
107
|
+
}
|
|
108
|
+
const startingSection = findStartingCloudSection(sections, lines.map(cloudProviderId).find(Boolean) ?? null);
|
|
109
|
+
if (startingSection && cloudStatus(lines) === "ended") {
|
|
110
|
+
startingSection.lines.push(...lines);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
sections.push({ kind: "cloud", lines });
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!isLoadmillLine(current)) {
|
|
118
|
+
sections.push({ kind: "log", line: current, visibleIndex: index });
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const previousLoadmill = findPreviousSection(sections, "loadmill");
|
|
123
|
+
if (previousLoadmill && shouldAppendToPreviousLoadmillBlock(current, previousLoadmill.lines)) {
|
|
124
|
+
previousLoadmill.lines.push(current);
|
|
125
|
+
index += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const lines = [current];
|
|
129
|
+
index += 1;
|
|
130
|
+
while (index < logs.length && (isLoadmillLine(logs[index]) || isLoadmillContinuationLine(logs[index]))) {
|
|
131
|
+
lines.push(logs[index++]);
|
|
132
|
+
}
|
|
133
|
+
sections.push({ kind: "loadmill", lines });
|
|
134
|
+
}
|
|
135
|
+
return sections;
|
|
136
|
+
}
|
|
@@ -123,7 +123,7 @@ export function resolveRuntimeCredentials({ openAiApiKey = null, loadmillApiToke
|
|
|
123
123
|
loadmillBaseUrl: normalizeLoadmillSiteBaseUrl(loadmillBaseUrl),
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
-
export function resolveRuntimeConfig({ llmProviderMode = null, cuaModel = null, explainMode =
|
|
126
|
+
export function resolveRuntimeConfig({ llmProviderMode = null, cuaModel = null, explainMode = true, stepDelayMs = undefined, contextOptimizationEnabled = undefined, contextOptimizationThreshold = undefined, promptCustomizations = null, appContext = null, } = {}) {
|
|
127
127
|
const normalizedCuaModel = typeof cuaModel === "string" && LOADMILL_MODEL_ALIAS_SET.has(cuaModel)
|
|
128
128
|
? cuaModel
|
|
129
129
|
: resolveCuaModel(cuaModel);
|
|
@@ -133,7 +133,7 @@ export function resolveRuntimeConfig({ llmProviderMode = null, cuaModel = null,
|
|
|
133
133
|
? "loadmill"
|
|
134
134
|
: normalizedProviderMode,
|
|
135
135
|
cuaModel: normalizedCuaModel,
|
|
136
|
-
explainMode: explainMode
|
|
136
|
+
explainMode: explainMode !== false,
|
|
137
137
|
stepDelayMs: normalizeStepDelayMs(Number(stepDelayMs)),
|
|
138
138
|
contextOptimizationEnabled: normalizeContextOptimizationEnabled(contextOptimizationEnabled),
|
|
139
139
|
contextOptimizationThreshold: normalizeContextOptimizationThreshold(contextOptimizationThreshold),
|