@loadmill/droid-cua 2.16.0 → 2.18.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.
Files changed (39) hide show
  1. package/README.md +18 -0
  2. package/build/index.js +88 -1
  3. package/build/src/browser/playwright/actions.js +17 -8
  4. package/build/src/cli/headless-execution-config.js +21 -0
  5. package/build/src/cli/headless-report.js +113 -0
  6. package/build/src/cli/headless-runner.js +57 -11
  7. package/build/src/cli/scenario-planner.js +1 -0
  8. package/build/src/commands/help.js +4 -0
  9. package/build/src/commands/run.js +2 -1
  10. package/build/src/core/execution-engine.js +57 -2
  11. package/build/src/core/prompts.js +1 -1
  12. package/build/src/core/report/generate-html-report.js +88 -0
  13. package/build/src/core/report/parse-run-report.js +222 -0
  14. package/build/src/core/report/render-html-report.js +317 -0
  15. package/build/src/core/report/report-file-name.js +31 -0
  16. package/build/src/core/report/run-log-model.js +144 -0
  17. package/build/src/core/report/run-report-model.js +143 -0
  18. package/build/src/core/scenario-planner.js +85 -0
  19. package/build/src/core/secrets.js +116 -0
  20. package/build/src/core/test-data-import.js +62 -0
  21. package/build/src/core/test-data.js +103 -0
  22. package/build/src/core/tool-handlers/close-current-tab.js +3 -3
  23. package/build/src/core/tool-handlers/navigate.js +3 -3
  24. package/build/src/core/tool-handlers/preview-click.js +6 -6
  25. package/build/src/core/tool-handlers/type-sensitive-text.js +62 -0
  26. package/build/src/core/tool-handlers/utils.js +3 -3
  27. package/build/src/device/android/actions.js +13 -8
  28. package/build/src/device/cloud/actions.js +17 -8
  29. package/build/src/device/ios/actions.js +17 -8
  30. package/build/src/device/openai.js +17 -0
  31. package/build/src/device/sensitive-action.js +61 -0
  32. package/build/src/modes/design-mode-ink.js +3 -1
  33. package/build/src/modes/design-mode.js +3 -1
  34. package/build/src/modes/execution-mode.js +1 -0
  35. package/build/src/prompts/design.js +4 -3
  36. package/build/src/prompts/execution.js +27 -1
  37. package/build/src/prompts/scenario-planner.js +48 -0
  38. package/build/src/prompts/test-data-import.js +49 -0
  39. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { logger } from "../../utils/logger.js";
2
2
  import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
3
3
  import { getConfiguredStepDelayMs } from "../../utils/step-delay.js";
4
4
  import { buildActionEventMeta } from "../action-event-meta.js";
5
+ import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
5
6
  import { buildMobileKeypressPayload, getCloudAndroidTransport, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
6
7
  import { getActiveSession, getDevicePixelRatio } from "./connection.js";
7
8
  import { resolveScrollGesture } from "../scroll-gesture.js";
@@ -33,7 +34,8 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
33
34
  deviceId,
34
35
  actionType: action?.type,
35
36
  scale,
36
- text: typeof action?.text === "string" ? truncateForDebug(action.text, 300) : undefined,
37
+ text: buildDebugTextField(action, truncateForDebug),
38
+ ...buildSensitiveDebugFields(action),
37
39
  keyCount: Array.isArray(action?.keys) ? action.keys.length : 0,
38
40
  pathPoints: Array.isArray(action?.path) ? action.path.length : 0
39
41
  });
@@ -48,7 +50,11 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
48
50
  break;
49
51
  }
50
52
  case "type": {
51
- addOutput({ type: "action", text: `Typing text: ${action.text}`, ...meta({ text: action.text }) });
53
+ addOutput({
54
+ type: "action",
55
+ text: buildTypeActionText(action, action.text),
56
+ ...meta(buildTypeActionPayload(action, action.text))
57
+ });
52
58
  await session.client.type(session.sessionId, action.text);
53
59
  break;
54
60
  }
@@ -133,12 +139,14 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
133
139
  default:
134
140
  addOutput({ type: "info", text: `Unknown action: ${JSON.stringify(action)}` });
135
141
  }
142
+ return { success: true };
136
143
  }
137
144
  catch (error) {
145
+ const outputError = buildActionErrorOutput(action, error);
138
146
  logger.error("Cloud action execution error", {
139
- action,
140
- message: error.message,
141
- stack: error.stack,
147
+ action: actionForErrorLog(action),
148
+ message: outputError.message,
149
+ stack: isSensitiveAction(action) ? undefined : error.stack,
142
150
  providerId: session.providerId,
143
151
  platform
144
152
  });
@@ -152,11 +160,11 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
152
160
  source: session.providerId,
153
161
  operation: "action.execute",
154
162
  actionType: action?.type,
155
- message: error.message
163
+ message: outputError.message
156
164
  });
157
165
  addOutput({
158
166
  type: "error",
159
- text: `Error executing action: ${error.message}`,
167
+ text: outputError.text,
160
168
  eventType: "error",
161
169
  actionType: action?.type,
162
170
  runId: context?.runId,
@@ -164,11 +172,12 @@ export async function handleModelAction(deviceId, action, scale = 1.0, context =
164
172
  instructionIndex: context?.instructionIndex,
165
173
  payload: {
166
174
  eventId: context?.eventId,
167
- message: error.message,
175
+ message: outputError.message,
168
176
  platform,
169
177
  source: session.providerId
170
178
  }
171
179
  });
172
180
  addOutput({ type: "info", text: "Full error details have been logged to the debug log." });
181
+ return { success: false, error: outputError.message };
173
182
  }
174
183
  }
@@ -6,6 +6,7 @@
6
6
  import * as appium from "./appium-client.js";
7
7
  import { buildActionEventMeta } from "../action-event-meta.js";
8
8
  import { buildMobileKeypressPayload, getIosTransport, normalizeMobileKeypress } from "../mobile-keypress.js";
9
+ import { actionForErrorLog, buildActionErrorOutput, buildDebugTextField, buildSensitiveDebugFields, buildTypeActionPayload, buildTypeActionText, isSensitiveAction, } from "../sensitive-action.js";
9
10
  import { getActiveSession, getDevicePixelRatio } from "./connection.js";
10
11
  import { logger } from "../../utils/logger.js";
11
12
  import { emitDesktopDebug, truncateForDebug } from "../../utils/desktop-debug.js";
@@ -41,7 +42,8 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
41
42
  simulatorId,
42
43
  actionType: action?.type,
43
44
  scale,
44
- text: typeof action?.text === "string" ? truncateForDebug(action.text, 300) : undefined,
45
+ text: buildDebugTextField(action, truncateForDebug),
46
+ ...buildSensitiveDebugFields(action),
45
47
  keyCount: Array.isArray(action?.keys) ? action.keys.length : 0,
46
48
  pathPoints: Array.isArray(action?.path) ? action.path.length : 0
47
49
  });
@@ -59,7 +61,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
59
61
  break;
60
62
  }
61
63
  case "type": {
62
- addOutput({ type: "action", text: `Typing text: ${action.text}`, ...meta({ text: action.text }) });
64
+ addOutput({
65
+ type: "action",
66
+ text: buildTypeActionText(action, action.text),
67
+ ...meta(buildTypeActionPayload(action, action.text))
68
+ });
63
69
  await appium.type(session.sessionId, action.text);
64
70
  break;
65
71
  }
@@ -138,12 +144,14 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
138
144
  default:
139
145
  addOutput({ type: "info", text: `Unknown action: ${JSON.stringify(action)}` });
140
146
  }
147
+ return { success: true };
141
148
  }
142
149
  catch (error) {
150
+ const outputError = buildActionErrorOutput(action, error);
143
151
  logger.error("iOS action execution error", {
144
- action,
145
- message: error.message,
146
- stack: error.stack,
152
+ action: actionForErrorLog(action),
153
+ message: outputError.message,
154
+ stack: isSensitiveAction(action) ? undefined : error.stack,
147
155
  });
148
156
  emitDesktopDebug("device.error", "device", {
149
157
  runId: context?.runId,
@@ -154,11 +162,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
154
162
  platform: "ios",
155
163
  operation: "action.execute",
156
164
  actionType: action?.type,
157
- message: error.message
165
+ message: outputError.message
158
166
  });
159
167
  addOutput({
160
168
  type: "error",
161
- text: `Error executing action: ${error.message}`,
169
+ text: outputError.text,
162
170
  eventType: "error",
163
171
  actionType: action?.type,
164
172
  runId: context?.runId,
@@ -166,10 +174,11 @@ export async function handleModelAction(simulatorId, action, scale = 1.0, contex
166
174
  instructionIndex: context?.instructionIndex,
167
175
  payload: {
168
176
  eventId: context?.eventId,
169
- message: error.message,
177
+ message: outputError.message,
170
178
  platform: "ios"
171
179
  }
172
180
  });
173
181
  addOutput({ type: "info", text: "Full error details have been logged to the debug log." });
182
+ return { success: false, error: outputError.message };
174
183
  }
175
184
  }
@@ -107,6 +107,20 @@ const loadmillTool = {
107
107
  additionalProperties: false,
108
108
  },
109
109
  };
110
+ const typeSensitiveTextTool = {
111
+ type: "function",
112
+ name: "type_sensitive_text",
113
+ description: "Type a secret value by key without exposing the value to the model. Use this only for secret keys listed in the prompt.",
114
+ strict: true,
115
+ parameters: {
116
+ type: "object",
117
+ properties: {
118
+ key: { type: "string" },
119
+ },
120
+ required: ["key"],
121
+ additionalProperties: false,
122
+ },
123
+ };
110
124
  const testDesignCompleteTool = {
111
125
  type: "function",
112
126
  name: "test_design_complete",
@@ -156,6 +170,9 @@ export function buildFunctionTools({ aimAssistMode = DEFAULT_AIM_ASSIST_MODE, to
156
170
  if (toolCapabilities.loadmill) {
157
171
  tools.push(loadmillTool);
158
172
  }
173
+ if (toolCapabilities.typeSensitiveText) {
174
+ tools.push(typeSensitiveTextTool);
175
+ }
159
176
  if (toolCapabilities.testDesignComplete) {
160
177
  tools.push(testDesignCompleteTool);
161
178
  }
@@ -0,0 +1,61 @@
1
+ export function isSensitiveAction(action) {
2
+ return action?.sensitive === true;
3
+ }
4
+ export function getSensitiveKey(action) {
5
+ return typeof action?.sensitiveKey === "string" && action.sensitiveKey.trim()
6
+ ? action.sensitiveKey.trim()
7
+ : "secret";
8
+ }
9
+ export function buildTypeActionText(action, text) {
10
+ if (isSensitiveAction(action)) {
11
+ return `Typing secret: ${getSensitiveKey(action)}`;
12
+ }
13
+ return `Typing text: ${text}`;
14
+ }
15
+ export function buildTypeActionPayload(action, text) {
16
+ if (isSensitiveAction(action)) {
17
+ return {
18
+ sensitive: true,
19
+ secretKey: getSensitiveKey(action),
20
+ };
21
+ }
22
+ return { text };
23
+ }
24
+ export function buildDebugTextField(action, truncate) {
25
+ if (isSensitiveAction(action)) {
26
+ return undefined;
27
+ }
28
+ return typeof action?.text === "string" ? truncate(action.text, 300) : undefined;
29
+ }
30
+ export function buildSensitiveDebugFields(action) {
31
+ if (!isSensitiveAction(action)) {
32
+ return {};
33
+ }
34
+ return {
35
+ sensitive: true,
36
+ secretKey: getSensitiveKey(action),
37
+ };
38
+ }
39
+ export function actionForErrorLog(action) {
40
+ if (!isSensitiveAction(action)) {
41
+ return action;
42
+ }
43
+ return {
44
+ type: action?.type,
45
+ sensitive: true,
46
+ secretKey: getSensitiveKey(action),
47
+ };
48
+ }
49
+ export function buildActionErrorOutput(action, error) {
50
+ if (isSensitiveAction(action)) {
51
+ const key = getSensitiveKey(action);
52
+ return {
53
+ text: `Error typing secret: ${key}`,
54
+ message: `Sensitive typing failed for ${key}.`,
55
+ };
56
+ }
57
+ return {
58
+ text: `Error executing action: ${error.message}`,
59
+ message: error.message,
60
+ };
61
+ }
@@ -33,7 +33,8 @@ export class DesignModeInk {
33
33
  async start() {
34
34
  const addOutput = this.context.addOutput;
35
35
  // Set design mode system prompt
36
- const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
36
+ const availableSecretKeys = this.engine.getAvailableSecretKeys();
37
+ const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, "", { availableSecretKeys });
37
38
  this.baseDesignPrompt = designPrompt;
38
39
  this.session.setSystemPrompt(designPrompt);
39
40
  this.engine.toolCapabilities = {
@@ -41,6 +42,7 @@ export class DesignModeInk {
41
42
  navigate: this.session?.deviceInfo?.platform === "browser",
42
43
  closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
43
44
  loadmill: false,
45
+ typeSensitiveText: this.engine.hasSecrets(),
44
46
  testDesignComplete: true,
45
47
  };
46
48
  this.engine.baseSystemPrompt = designPrompt;
@@ -30,7 +30,8 @@ export class DesignMode {
30
30
  */
31
31
  async start(context) {
32
32
  // Set design mode system prompt
33
- const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {});
33
+ const availableSecretKeys = this.engine.getAvailableSecretKeys();
34
+ const designPrompt = buildDesignModePrompt(this.session.deviceInfo, {}, "", { availableSecretKeys });
34
35
  this.baseDesignPrompt = designPrompt;
35
36
  this.session.setSystemPrompt(designPrompt);
36
37
  this.engine.toolCapabilities = {
@@ -38,6 +39,7 @@ export class DesignMode {
38
39
  navigate: this.session?.deviceInfo?.platform === "browser",
39
40
  closeCurrentTab: this.session?.deviceInfo?.platform === "browser",
40
41
  loadmill: false,
42
+ typeSensitiveText: this.engine.hasSecrets(),
41
43
  testDesignComplete: true,
42
44
  };
43
45
  this.engine.baseSystemPrompt = designPrompt;
@@ -70,6 +70,7 @@ export class ExecutionMode {
70
70
  navigate: this.isBrowserSession,
71
71
  closeCurrentTab: this.isBrowserSession,
72
72
  loadmill: true,
73
+ typeSensitiveText: this.engine.hasSecrets(),
73
74
  };
74
75
  this.engine.baseSystemPrompt = this.initialSystemText;
75
76
  this.engine.isHeadlessMode = this.isHeadlessMode;
@@ -1,9 +1,10 @@
1
1
  import { appendCustomSections, buildBaseSystemPrompt, isBrowserTarget } from "./base.js";
2
- import { buildAppContextSection } from "./execution.js";
3
- export function buildDesignModePrompt(deviceInfo, customInstructions = {}, appContextText = "") {
2
+ import { buildAppContextSection, buildSecretKeysContextSection } from "./execution.js";
3
+ export function buildDesignModePrompt(deviceInfo, customInstructions = {}, appContextText = "", options = {}) {
4
4
  const designCustomText = typeof customInstructions.designModeInstructions === "string" ? customInstructions.designModeInstructions.trim() : "";
5
5
  const basePrompt = buildBaseSystemPrompt(deviceInfo, customInstructions);
6
6
  const appContextSection = buildAppContextSection(appContextText);
7
+ const secretKeysContextSection = buildSecretKeysContextSection(options.availableSecretKeys);
7
8
  const browserTarget = isBrowserTarget(deviceInfo);
8
9
  const prompt = `${basePrompt}
9
10
 
@@ -93,7 +94,7 @@ WRONG Example (DON'T DO THIS):
93
94
  3. etc...
94
95
  \`\`\`
95
96
 
96
- Remember: You are autonomous. Explore confidently. Generate simple, executable test scripts.${appContextSection ? `\n\n${appContextSection}` : ""}
97
+ Remember: You are autonomous. Explore confidently. Generate simple, executable test scripts.${appContextSection ? `\n\n${appContextSection}` : ""}${secretKeysContextSection ? `\n\n${secretKeysContextSection}` : ""}
97
98
  `;
98
99
  return appendCustomSections(prompt, [
99
100
  { title: "Base Prompt Instructions", text: customInstructions.basePromptInstructions },
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_AIM_ASSIST_MODE } from "../core/aim-assist.js";
2
+ import { normalizeSecretKeys } from "../core/secrets.js";
2
3
  import { appendCustomSections, buildBaseSystemPrompt, isBrowserTarget } from "./base.js";
3
4
  export function buildAppContextSection(text) {
4
5
  const trimmed = typeof text === "string" ? text.trim() : "";
@@ -11,11 +12,36 @@ Use this to understand screen layouts, terminology, navigation, and expected beh
11
12
 
12
13
  ${trimmed}`;
13
14
  }
15
+ export function buildTestDataContextSection(text) {
16
+ const trimmed = typeof text === "string" ? text.trim() : "";
17
+ if (!trimmed) {
18
+ return "";
19
+ }
20
+ return `TEST DATA FOR THIS RUN:
21
+ The following values are test data for this execution. Use them as literal run data when performing the test.
22
+ They do not replace or override the test instructions.
23
+
24
+ ${trimmed}`;
25
+ }
26
+ export function buildSecretKeysContextSection(keys = []) {
27
+ const normalized = normalizeSecretKeys(keys);
28
+ if (normalized.length === 0) {
29
+ return "";
30
+ }
31
+ return `SECRET KEYS FOR THIS RUN:
32
+ The following secret keys are available. You can see only the keys, not the values.
33
+ Use the \`type_sensitive_text\` tool when you need to enter passwords, OTPs, tokens, API keys, or other hidden credentials.
34
+ Never ask the user to reveal these values and never type placeholder text when a matching secret key is available.
35
+
36
+ ${normalized.map((key) => `- ${key}`).join("\n")}`;
37
+ }
14
38
  export function buildExecutionModePrompt(deviceInfo, customInstructions = {}, appContextText = "", options = {}) {
15
39
  const executionCustomText = typeof customInstructions.executionModeInstructions === "string" ? customInstructions.executionModeInstructions.trim() : "";
16
40
  const aimAssistMode = typeof options.aimAssistMode === "string" ? options.aimAssistMode : DEFAULT_AIM_ASSIST_MODE;
17
41
  const basePrompt = buildBaseSystemPrompt(deviceInfo, { aimAssistMode });
18
42
  const appContextSection = buildAppContextSection(appContextText);
43
+ const testDataContextSection = buildTestDataContextSection(options.testDataContext);
44
+ const secretKeysContextSection = buildSecretKeysContextSection(options.availableSecretKeys);
19
45
  const browserTarget = isBrowserTarget(deviceInfo);
20
46
  const prompt = `${basePrompt}
21
47
 
@@ -52,7 +78,7 @@ Your process:
52
78
  7. Stop immediately - no commentary, no questions
53
79
 
54
80
  Each instruction is independent. Do not reference previous instructions or ask about next steps.
55
- ${appContextSection ? `\n\n${appContextSection}` : ""}`;
81
+ ${appContextSection ? `\n\n${appContextSection}` : ""}${testDataContextSection ? `\n\n${testDataContextSection}` : ""}${secretKeysContextSection ? `\n\n${secretKeysContextSection}` : ""}`;
56
82
  return appendCustomSections(prompt, [
57
83
  { title: "Base Prompt Instructions", text: customInstructions.basePromptInstructions },
58
84
  { title: "Execution Mode Instructions", text: executionCustomText }
@@ -0,0 +1,48 @@
1
+ function formatCandidateTests(tests) {
2
+ return tests.map((test, index) => {
3
+ const content = Array.isArray(test.instructions) ? test.instructions.join("\n") : "";
4
+ return [
5
+ `TEST ${index + 1}`,
6
+ `Name: ${test.testName}`,
7
+ "Instructions:",
8
+ content || "(empty)",
9
+ ].join("\n");
10
+ }).join("\n\n---\n\n");
11
+ }
12
+ export function buildScenarioPlannerMessages({ scenarioPrompt, tests, appContextText = "", testDataText = "", }) {
13
+ return [
14
+ {
15
+ role: "system",
16
+ content: [
17
+ "You prepare concrete test executions for a computer-using agent.",
18
+ "The agent runs manual-style app tests: it receives a test brief, app context, and optional run data, then operates the app UI.",
19
+ "Your job is to turn the user's scenario request into the exact ordered cases the agent should run.",
20
+ "Return JSON only with this shape:",
21
+ "{\"cases\":[{\"testName\":\"exact-test-name.dcua\",\"caseName\":\"test-name.dcua / data label\",\"testDataContext\":\"raw data text for this case\"}]}",
22
+ "Rules:",
23
+ "- testName must exactly match one of the provided candidate test names.",
24
+ "- Create an ordered flat list of cases to run.",
25
+ "- caseName is an ephemeral display name for logs and reports. Keep it short and human-readable.",
26
+ "- testDataContext must contain only the concrete data relevant to that case.",
27
+ "- Do not invent data that was not provided in test data, context, or the scenario prompt.",
28
+ "- If no extra data is needed for a case, use an empty string for testDataContext.",
29
+ ].join("\n")
30
+ },
31
+ {
32
+ role: "user",
33
+ content: [
34
+ "Scenario prompt:",
35
+ scenarioPrompt,
36
+ "",
37
+ "App context:",
38
+ appContextText && appContextText.trim() ? appContextText.trim() : "(none)",
39
+ "",
40
+ "Project test data:",
41
+ testDataText && testDataText.trim() ? testDataText.trim() : "(none)",
42
+ "",
43
+ "Candidate tests:",
44
+ formatCandidateTests(tests),
45
+ ].join("\n")
46
+ }
47
+ ];
48
+ }
@@ -0,0 +1,49 @@
1
+ import { DEFAULT_TEST_DATA_TEMPLATE, isDefaultTestDataTemplate } from "../core/test-data.js";
2
+ export function buildTestDataImportMessages({ currentContent = "", importedFileName, importedFileText, instructions = "", }) {
3
+ const currentIsTemplateOnly = isDefaultTestDataTemplate(currentContent);
4
+ const userInstructions = typeof instructions === "string" && instructions.trim()
5
+ ? instructions.trim()
6
+ : "(none)";
7
+ return [
8
+ {
9
+ role: "system",
10
+ content: [
11
+ "You convert imported data into durable project test data for computer-using-agent scenario runs.",
12
+ "The durable format is test-data.md: readable markdown that describes concrete entities and run data the agent can use.",
13
+ "Return JSON only with this shape:",
14
+ "{\"content\":\"# Test Data\\n...complete markdown...\"}",
15
+ "Rules:",
16
+ "- Return the complete replacement content for test-data.md, not a patch.",
17
+ "- Do not invent values that are not present in the current file, imported file, or user instructions.",
18
+ "- If currentContent is template-only, replace it with real imported data and do not keep commented template examples.",
19
+ "- If currentContent is not template-only, preserve its useful structure and add or update entities within it.",
20
+ "- For matrix-like CSV rows, prefer splitting repeated dimensions into separate entities when the structure is clear.",
21
+ "- If rows are indivisible scenarios or examples, keep them grouped as Test Data Entries or Use Case Data.",
22
+ "- User instructions can guide structure and naming, but content must remain concrete data, not test instructions.",
23
+ "- Output markdown only inside the JSON content string; do not use code fences.",
24
+ ].join("\n")
25
+ },
26
+ {
27
+ role: "user",
28
+ content: [
29
+ "Current test-data.md status:",
30
+ currentIsTemplateOnly ? "template-only" : "existing user content",
31
+ "",
32
+ "Default template used for template-only detection:",
33
+ DEFAULT_TEST_DATA_TEMPLATE.trimEnd(),
34
+ "",
35
+ "Current test-data.md content:",
36
+ String(currentContent ?? "").trim() ? String(currentContent) : "(empty)",
37
+ "",
38
+ "Imported file:",
39
+ importedFileName,
40
+ "",
41
+ "Imported file text:",
42
+ importedFileText,
43
+ "",
44
+ "User import instructions:",
45
+ userInstructions,
46
+ ].join("\n")
47
+ }
48
+ ];
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loadmill/droid-cua",
3
- "version": "2.16.0",
3
+ "version": "2.18.0",
4
4
  "description": "AI-powered Android testing agent using OpenAI's computer-use model and ADB",
5
5
  "main": "build/index.js",
6
6
  "type": "module",