testronaut 1.1.1 → 1.1.2

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.
@@ -13,7 +13,36 @@ function pickArg(args, keys) {
13
13
  return null;
14
14
  }
15
15
 
16
- export function summarizeToolCall(fnName, args = {}) {
16
+ // core/turnIntent.js
17
+ const ICONS = {
18
+ navigate: '🧭',
19
+ click_text: '🖱️',
20
+ click: '🖱️',
21
+ type: '⌨️',
22
+ fill: '⌨️',
23
+ press: '⌨️',
24
+ hover: '🪄',
25
+ wait_for_selector: '⏳',
26
+ wait: '⏳',
27
+ screenshot: '📸',
28
+ get_dom: '🧩',
29
+ check_text: '🔍',
30
+ scroll: '↕️',
31
+ upload_file: '📤',
32
+ download_file: '📥',
33
+ expand_menu: '📂',
34
+ set_viewport: '🖥️',
35
+ default: '⚙️',
36
+ };
37
+
38
+ const EMOJI_ON_DEFAULT =
39
+ process.env.TESTRONAUT_EMOJI === '1' || process.env.TESTRONAUT_EMOJI === 'true';
40
+
41
+ const withIcon = (name, s, { emoji = EMOJI_ON_DEFAULT } = {}) =>
42
+ emoji ? `${ICONS[name] || ICONS.default} ${s}` : s;
43
+
44
+
45
+ export function summarizeToolCall(fnName, args = {}, opts = {}) {
17
46
  const safeArgs = redactArgs(fnName, args);
18
47
 
19
48
  const selector = pickArg(safeArgs, ['selector','xpath','css','role','testId']);
@@ -29,33 +58,35 @@ export function summarizeToolCall(fnName, args = {}) {
29
58
  const tmo = timeout ? ` [≤${timeout}ms]` : '';
30
59
 
31
60
  switch (fnName) {
32
- case 'navigate': return `Navigate to ${truncateMiddle(url || '(no url)', 80)}`;
33
- case 'click_text': return `Click text${val}`;
34
- case 'click': return `Click element${sel}`;
61
+ case 'navigate': return withIcon('navigate', `Navigate to ${truncateMiddle(url || '(no url)', 80)}`, opts);
62
+ case 'click_text': return withIcon('click_text', `Click text${val}`, opts);
63
+ case 'click': return withIcon('click', `Click element${sel}`, opts);
35
64
  case 'type':
36
- case 'fill': return `Type${val}${selector ? ` into ${truncateMiddle(selector, 60)}` : ''}`;
37
- case 'press': return `Press key${val || ''}${selector ? ` in ${truncateMiddle(selector, 60)}` : ''}`;
38
- case 'hover': return `Hover over${sel || val || ''}`;
39
- case 'wait_for_selector': return `Wait for${sel}${tmo}`;
40
- case 'wait': return `Wait${tmo || ''}`;
41
- case 'screenshot': return `Capture screenshot${selector ? ` of ${truncateMiddle(selector, 60)}` : ''}`;
42
- case 'get_dom': return `Inspect DOM (focused extract)`;
43
- case 'check_text': return `Check page for${val || ' specific text'}`;
44
- case 'scroll': return `Scroll to coords (${x ?? '?'}, ${y ?? '?'})`;
45
- case 'upload_file': return `Upload file ${truncateMiddle(file || '(unknown)', 60)}${selector ? ` via ${truncateMiddle(selector, 60)}` : ''}`;
46
- case 'download_file': return `Download file to ${truncateMiddle(file || '(unknown)', 60)}`;
47
- case 'expand_menu': return `Expand menu${sel || ''}`;
48
- case 'set_viewport': return `Set viewport ${safeArgs?.width}×${safeArgs?.height}`;
65
+ case 'fill': return withIcon('type', `Type${val}${selector ? ` into ${truncateMiddle(selector, 60)}` : ''}`, opts);
66
+ case 'press': return withIcon('press', `Press key${val || ''}${selector ? ` in ${truncateMiddle(selector, 60)}` : ''}`, opts);
67
+ case 'hover': return withIcon('hover', `Hover over${sel || val || ''}`, opts);
68
+ case 'wait_for_selector':
69
+ return withIcon('wait_for_selector', `Wait for${sel}${tmo}`, opts);
70
+ case 'wait': return withIcon('wait', `Wait${tmo || ''}`, opts);
71
+ case 'screenshot': return withIcon('screenshot', `Capture screenshot${selector ? ` of ${truncateMiddle(selector, 60)}` : ''}`, opts);
72
+ case 'get_dom': return withIcon('get_dom', `Inspect DOM (focused extract)`, opts);
73
+ case 'check_text': return withIcon('check_text', `Check page for${val || ' specific text'}`, opts);
74
+ case 'scroll': return withIcon('scroll', `Scroll to coords (${x ?? '?'}, ${y ?? '?'})`, opts);
75
+ case 'upload_file': return withIcon('upload_file', `Upload file ${truncateMiddle(file || '(unknown)', 60)}${selector ? ` via ${truncateMiddle(selector, 60)}` : ''}`, opts);
76
+ case 'download_file':
77
+ return withIcon('download_file', `Download file to ${truncateMiddle(file || '(unknown)', 60)}`, opts);
78
+ case 'expand_menu': return withIcon('expand_menu', `Expand menu${sel || ''}`, opts);
79
+ case 'set_viewport': return withIcon('set_viewport', `Set viewport ${safeArgs?.width}×${safeArgs?.height}`, opts);
49
80
  default:
50
- return `Run ${fnName} with ${truncateMiddle(JSON.stringify(safeArgs ?? {}), 80)}`;
81
+ return withIcon('default', `Run ${fnName} with ${truncateMiddle(JSON.stringify(safeArgs ?? {}), 80)}`, opts);
51
82
  }
52
83
  }
53
84
 
54
- export function summarizeTurnIntentFromMessage(msg) {
85
+ export function summarizeTurnIntentFromMessage(msg, opts = {}) {
55
86
  try {
56
87
  if (msg?.tool_calls?.length) {
57
88
  return msg.tool_calls
58
- .map(c => summarizeToolCall(c.function?.name || 'unknown', JSON.parse(c.function?.arguments || '{}')))
89
+ .map(c => summarizeToolCall(c.function?.name || 'unknown', JSON.parse(c.function?.arguments || '{}'), opts))
59
90
  .join(' → ');
60
91
  }
61
92
  if (msg?.content) return `Reasoning: ${truncateMiddle(msg.content, 120)}`;
package/core/turnLoop.js CHANGED
@@ -180,10 +180,14 @@ export const turnLoop = async (
180
180
 
181
181
  const msg = response.choices[0].message;
182
182
 
183
- const planLine = summarizeTurnIntentFromMessage(msg);
184
- currentStep.summary = planLine;
185
- currentStep.events.unshift(`📝 Plan: ${planLine}`);
186
- console.log(`📝 Plan: ${planLine}`);
183
+ // Plain (no emoji) for storage:
184
+ const planPlain = summarizeTurnIntentFromMessage(msg, { emoji: true });
185
+ // Emoji (toggleable) for console/event log:
186
+ const planDisplay = summarizeTurnIntentFromMessage(msg, { /* emoji default via env */ });
187
+
188
+ currentStep.summary = planPlain; // stored in JSON
189
+ currentStep.events.unshift(`📝 Plan: ${planDisplay}`);
190
+ console.log(`📝 Plan: ${planDisplay}`);
187
191
 
188
192
  if (msg.tool_calls?.length) {
189
193
  console.log('Processing tool calls...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "./bin/cli.js"
@@ -64,6 +64,9 @@ export function generateHtmlReport(report, outputPath) {
64
64
  <span class="name">${prettyTitle}</span>
65
65
  <span class="status ${statusClass}">${badge(m.status)}</span>
66
66
  <span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s</span>
67
+ <span class="toolbar">
68
+ <button class="btn-mini toggle" data-scope="submission" aria-label="Expand">▼</button>
69
+ </span>
67
70
  </summary>
68
71
  <div class="steps">
69
72
  ${stepItems || '<div class="empty">No steps recorded.</div>'}
@@ -101,12 +104,16 @@ export function generateHtmlReport(report, outputPath) {
101
104
  <span class="name">${esc(missionName)}</span>
102
105
  <span class="status ${status === 'failed' ? 'bad' : 'ok'}">${badge(status)}</span>
103
106
  <span class="meta">submissions: ${subs.length} • steps: ${totalSteps} • duration: ${groupDur}s</span>
107
+ <span class="toolbar">
108
+ <button class="btn-mini toggle" data-scope="mission" aria-label="Expand">▼</button>
109
+ </span>
104
110
  </summary>
105
111
  <div class="group-body">
106
112
  ${subs.map(submissionBlock).join('')}
107
113
  </div>
108
114
  </details>
109
115
  `;
116
+
110
117
  };
111
118
 
112
119
  const groupsHtml = Object.entries(grouped).map(([name, subs]) => missionGroupBlock(name, subs)).join('');
@@ -199,6 +206,22 @@ export function generateHtmlReport(report, outputPath) {
199
206
  .mission-group > summary:hover{ background: rgba(255,255,255,.08); }
200
207
 
201
208
  .group-body{ padding:12px; display:grid; gap:10px; }
209
+ // .toolbar{display:flex; gap:8px; margin-left:auto;}
210
+ // .btn-mini{
211
+ // font-size:11px; padding:4px 8px; border-radius:8px;
212
+ // background: rgba(255,255,255,.06); color: var(--text);
213
+ // border:1px solid var(--hairline); cursor:pointer;
214
+ // }
215
+ // .btn-mini:hover{ background: rgba(255,255,255,.10); }
216
+
217
+ .toolbar{display:flex; gap:8px; margin-left:auto;}
218
+ .btn-mini{
219
+ font-size:12px; padding:2px 8px; border-radius:8px;
220
+ background: rgba(255,255,255,.06); color: var(--text);
221
+ border:1px solid var(--hairline); cursor:pointer; line-height:1.2;
222
+ width:28px; text-align:center;
223
+ }
224
+ .btn-mini:hover{ background: rgba(255,255,255,.10); }
202
225
 
203
226
  .mission-submission > summary{
204
227
  font-weight:700; background: rgba(255,255,255,.06); border-top:1px solid var(--hairline);
@@ -268,6 +291,66 @@ export function generateHtmlReport(report, outputPath) {
268
291
  <div class="container">
269
292
  ${groupsHtml || '<div class="glass empty">No missions recorded.</div>'}
270
293
  </div>
294
+ <script>
295
+ (function () {
296
+ function setOpenAll(root, selector, open) {
297
+ root.querySelectorAll(selector).forEach(function (el) {
298
+ if (el && 'open' in el) el.open = open;
299
+ });
300
+ }
301
+ function areAllOpen(root, selector) {
302
+ var list = root.querySelectorAll(selector);
303
+ if (!list.length) return false;
304
+ for (var i=0;i<list.length;i++) { if (!list[i].open) return false; }
305
+ return true;
306
+ }
307
+ function refreshBtn(btn) {
308
+ var scope = btn.getAttribute('data-scope');
309
+ var root = btn.closest(scope === 'mission' ? '.mission-group' : '.mission-submission');
310
+ if (!root) return;
311
+ var allOpen = scope === 'mission'
312
+ ? (areAllOpen(root, '.mission-submission') && areAllOpen(root, '.mission-submission .step'))
313
+ : areAllOpen(root, '.step');
314
+ btn.textContent = allOpen ? '▲' : '▼';
315
+ btn.setAttribute('aria-label', allOpen ? 'Collapse' : 'Expand');
316
+ btn.dataset.open = allOpen ? 'true' : 'false';
317
+ }
318
+
319
+ // Click handler (single toggle)
320
+ document.addEventListener('click', function (e) {
321
+ var btn = e.target && e.target.closest && e.target.closest('button.toggle');
322
+ if (!btn) return;
323
+ e.preventDefault(); e.stopPropagation();
324
+
325
+ var scope = btn.getAttribute('data-scope');
326
+ var root = btn.closest(scope === 'mission' ? '.mission-group' : '.mission-submission');
327
+ if (!root) return;
328
+
329
+ var currentlyOpen = btn.dataset.open === 'true';
330
+ var nextOpen = !currentlyOpen;
331
+
332
+ if (scope === 'mission') {
333
+ root.open = true; // ensure visible
334
+ setOpenAll(root, '.mission-submission', nextOpen);
335
+ setOpenAll(root, '.mission-submission .step', nextOpen);
336
+ } else {
337
+ root.open = true;
338
+ setOpenAll(root, '.step', nextOpen);
339
+ }
340
+ refreshBtn(btn);
341
+ }, true);
342
+
343
+ // Keep arrows in sync if user toggles details manually
344
+ document.addEventListener('toggle', function () {
345
+ document.querySelectorAll('button.toggle').forEach(refreshBtn);
346
+ }, true);
347
+
348
+ // Initial refresh
349
+ document.querySelectorAll('button.toggle').forEach(refreshBtn);
350
+ })();
351
+ </script>
352
+
353
+
271
354
  </body>
272
355
  </html>`;
273
356