@veolab/discoverylab 1.1.0 → 1.2.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/dist/chunk-2OGZX6C4.js +588 -0
- package/dist/chunk-43U6UYV7.js +590 -0
- package/dist/chunk-4H2E3K2G.js +7638 -0
- package/dist/chunk-4KLG6DDE.js +334 -0
- package/dist/chunk-4NNTRJOI.js +7791 -0
- package/dist/chunk-5F76VWME.js +6397 -0
- package/dist/chunk-5NEFN42O.js +7791 -0
- package/dist/chunk-63MEQ6UH.js +7673 -0
- package/dist/chunk-C7QUR7XX.js +6397 -0
- package/dist/chunk-GGJJUCFK.js +7160 -0
- package/dist/chunk-GLHOY3NN.js +7805 -0
- package/dist/chunk-GSWHWEYC.js +1346 -0
- package/dist/chunk-HDKEQOF5.js +7788 -0
- package/dist/chunk-HZGSWVVS.js +7111 -0
- package/dist/chunk-I6YD3QFM.js +500 -0
- package/dist/chunk-IRKQG33A.js +7054 -0
- package/dist/chunk-KV7KDJ43.js +7639 -0
- package/dist/chunk-L4SA5F5W.js +6397 -0
- package/dist/chunk-MFFPQLU4.js +7102 -0
- package/dist/chunk-MJS2YKNR.js +6397 -0
- package/dist/chunk-NDBW6ELQ.js +7638 -0
- package/dist/chunk-P4S7ZY6G.js +7638 -0
- package/dist/chunk-PMTGGZ7R.js +6397 -0
- package/dist/chunk-PYUCY3U6.js +1340 -0
- package/dist/chunk-RDZDSOAL.js +7750 -0
- package/dist/chunk-SLNJEF32.js +91 -0
- package/dist/chunk-SR67SRIT.js +1336 -0
- package/dist/chunk-TAODYZ52.js +1393 -0
- package/dist/chunk-TBG76CYG.js +6395 -0
- package/dist/chunk-TJ3H23LL.js +362 -0
- package/dist/chunk-XIBF5LBD.js +6395 -0
- package/dist/chunk-XUKWS2CE.js +7805 -0
- package/dist/cli.js +6 -6
- package/dist/db-ADBEBNH6.js +35 -0
- package/dist/index.d.ts +170 -1
- package/dist/index.html +1168 -106
- package/dist/index.js +9 -7
- package/dist/playwright-ATDC4NYW.js +38 -0
- package/dist/playwright-E6EUFIJG.js +38 -0
- package/dist/playwright-R7Y5HREH.js +39 -0
- package/dist/server-2VKO76UK.js +14 -0
- package/dist/server-3BK2VFU7.js +13 -0
- package/dist/server-6IPHVUYT.js +14 -0
- package/dist/server-73P7M3QB.js +14 -0
- package/dist/server-BPVRW5LJ.js +14 -0
- package/dist/server-F3YPX6ET.js +13 -0
- package/dist/server-IOOZK4NP.js +14 -0
- package/dist/server-J52LMTBT.js +13 -0
- package/dist/server-NPZN3FWO.js +14 -0
- package/dist/server-O5FIAHSY.js +14 -0
- package/dist/server-P27BZXBL.js +14 -0
- package/dist/server-S6B5WUBT.js +14 -0
- package/dist/server-SRYNSGSP.js +14 -0
- package/dist/server-X3TLP6DX.js +14 -0
- package/dist/server-ZBPQ33V6.js +14 -0
- package/dist/setup-AQX4JQVR.js +17 -0
- package/dist/tools-2KPB37GK.js +178 -0
- package/dist/tools-3H6IOWXV.js +178 -0
- package/dist/tools-BUVCUCRL.js +178 -0
- package/dist/tools-HDNODRS6.js +178 -0
- package/dist/tools-L6PKKQPY.js +179 -0
- package/dist/tools-N5N2IO7V.js +178 -0
- package/dist/tools-TLCKABUW.js +178 -0
- package/package.json +1 -1
package/dist/index.html
CHANGED
|
@@ -1824,12 +1824,22 @@
|
|
|
1824
1824
|
justify-content: center;
|
|
1825
1825
|
padding-top: 15vh;
|
|
1826
1826
|
z-index: 1000;
|
|
1827
|
+
overscroll-behavior: contain;
|
|
1827
1828
|
}
|
|
1828
1829
|
|
|
1829
1830
|
.modal-overlay.active {
|
|
1830
1831
|
display: flex;
|
|
1831
1832
|
}
|
|
1832
1833
|
|
|
1834
|
+
.modal-overlay.code-modal-overlay {
|
|
1835
|
+
align-items: center;
|
|
1836
|
+
justify-content: center;
|
|
1837
|
+
padding: clamp(10px, 3vh, 24px);
|
|
1838
|
+
padding-top: clamp(10px, 3vh, 24px);
|
|
1839
|
+
background: rgba(4, 6, 10, 0.78);
|
|
1840
|
+
backdrop-filter: blur(2px);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1833
1843
|
.modal {
|
|
1834
1844
|
background: var(--bg-surface);
|
|
1835
1845
|
border: 1px solid var(--border);
|
|
@@ -2014,10 +2024,12 @@
|
|
|
2014
2024
|
max-width: 900px;
|
|
2015
2025
|
width: min(92vw, 900px);
|
|
2016
2026
|
max-height: 82vh;
|
|
2027
|
+
height: min(86vh, 860px);
|
|
2017
2028
|
display: flex;
|
|
2018
2029
|
flex-direction: column;
|
|
2019
2030
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(0, 0, 0, 0.02)), var(--bg-surface);
|
|
2020
2031
|
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
|
2032
|
+
overscroll-behavior: contain;
|
|
2021
2033
|
}
|
|
2022
2034
|
|
|
2023
2035
|
.code-modal .modal-header {
|
|
@@ -2053,12 +2065,21 @@
|
|
|
2053
2065
|
}
|
|
2054
2066
|
|
|
2055
2067
|
.code-modal-body {
|
|
2056
|
-
padding:
|
|
2068
|
+
padding: 12px 16px 0;
|
|
2057
2069
|
flex: 1;
|
|
2058
2070
|
overflow: hidden;
|
|
2059
2071
|
display: flex;
|
|
2060
2072
|
flex-direction: column;
|
|
2061
|
-
gap:
|
|
2073
|
+
gap: 10px;
|
|
2074
|
+
min-height: 0;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
.code-modal-panel {
|
|
2078
|
+
display: flex;
|
|
2079
|
+
flex-direction: column;
|
|
2080
|
+
flex: 1;
|
|
2081
|
+
min-height: 0;
|
|
2082
|
+
min-width: 0;
|
|
2062
2083
|
}
|
|
2063
2084
|
|
|
2064
2085
|
.code-editor {
|
|
@@ -2081,17 +2102,20 @@
|
|
|
2081
2102
|
.code-editor-container {
|
|
2082
2103
|
display: flex;
|
|
2083
2104
|
flex: 1;
|
|
2084
|
-
background:
|
|
2105
|
+
background: #070b11;
|
|
2085
2106
|
border: 1px solid var(--border);
|
|
2086
2107
|
border-radius: 10px;
|
|
2087
2108
|
overflow: hidden;
|
|
2088
2109
|
min-height: 320px;
|
|
2110
|
+
height: 100%;
|
|
2111
|
+
min-width: 0;
|
|
2112
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02);
|
|
2089
2113
|
}
|
|
2090
2114
|
|
|
2091
2115
|
.code-line-numbers {
|
|
2092
|
-
background:
|
|
2116
|
+
background: #0b1017;
|
|
2093
2117
|
color: var(--text-muted);
|
|
2094
|
-
padding: 14px 12px 14px 14px;
|
|
2118
|
+
padding: 14px 12px calc(14px + 4.8em) 14px;
|
|
2095
2119
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
2096
2120
|
font-size: 12px;
|
|
2097
2121
|
line-height: 1.6;
|
|
@@ -2099,16 +2123,16 @@
|
|
|
2099
2123
|
user-select: none;
|
|
2100
2124
|
border-right: 1px solid var(--border);
|
|
2101
2125
|
min-width: 45px;
|
|
2102
|
-
overflow: hidden;
|
|
2126
|
+
overflow-y: hidden;
|
|
2103
2127
|
white-space: pre;
|
|
2104
2128
|
}
|
|
2105
2129
|
|
|
2106
2130
|
.code-editor-textarea {
|
|
2107
2131
|
flex: 1;
|
|
2108
|
-
background:
|
|
2132
|
+
background: #070b11;
|
|
2109
2133
|
color: var(--text-primary);
|
|
2110
2134
|
border: none;
|
|
2111
|
-
padding: 14px 16px;
|
|
2135
|
+
padding: 14px 16px calc(14px + 4.8em) 16px;
|
|
2112
2136
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
2113
2137
|
font-size: 12px;
|
|
2114
2138
|
line-height: 1.6;
|
|
@@ -2116,12 +2140,22 @@
|
|
|
2116
2140
|
tab-size: 2;
|
|
2117
2141
|
outline: none;
|
|
2118
2142
|
overflow-y: auto;
|
|
2143
|
+
height: 100%;
|
|
2144
|
+
min-height: 0;
|
|
2145
|
+
box-sizing: border-box;
|
|
2146
|
+
scroll-padding-bottom: 4.8em;
|
|
2147
|
+
overscroll-behavior: contain;
|
|
2148
|
+
-webkit-overflow-scrolling: touch;
|
|
2119
2149
|
}
|
|
2120
2150
|
|
|
2121
2151
|
.code-editor-textarea:focus {
|
|
2122
2152
|
outline: none;
|
|
2123
2153
|
}
|
|
2124
2154
|
|
|
2155
|
+
.code-editor-textarea::selection {
|
|
2156
|
+
background: rgba(96, 165, 250, 0.28);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2125
2159
|
/* YAML Syntax Highlighting (via overlay) */
|
|
2126
2160
|
.yaml-key { color: #7dd3fc; }
|
|
2127
2161
|
.yaml-value { color: #fde68a; }
|
|
@@ -2140,6 +2174,16 @@
|
|
|
2140
2174
|
max-height: 180px;
|
|
2141
2175
|
overflow-y: auto;
|
|
2142
2176
|
color: #8b949e;
|
|
2177
|
+
overscroll-behavior: contain;
|
|
2178
|
+
-webkit-overflow-scrolling: touch;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
.code-vars-table-wrap {
|
|
2182
|
+
overflow: auto;
|
|
2183
|
+
border: 1px solid var(--border);
|
|
2184
|
+
border-radius: 8px;
|
|
2185
|
+
overscroll-behavior: contain;
|
|
2186
|
+
-webkit-overflow-scrolling: touch;
|
|
2143
2187
|
}
|
|
2144
2188
|
|
|
2145
2189
|
.code-modal-output .output-success { color: #4ade80; }
|
|
@@ -2179,6 +2223,7 @@
|
|
|
2179
2223
|
@media (max-width: 720px) {
|
|
2180
2224
|
.code-modal {
|
|
2181
2225
|
max-height: 88vh;
|
|
2226
|
+
height: min(92vh, 920px);
|
|
2182
2227
|
}
|
|
2183
2228
|
|
|
2184
2229
|
.code-modal-footer {
|
|
@@ -7047,12 +7092,24 @@
|
|
|
7047
7092
|
</button>
|
|
7048
7093
|
</div>
|
|
7049
7094
|
<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px;">
|
|
7050
|
-
<a href="https://ollama.ai" target="_blank" style="color: var(--accent);">Install Ollama</a> -
|
|
7095
|
+
<a href="https://ollama.ai" target="_blank" style="color: var(--accent);">Install Ollama</a> - Used for local text/coding and optional visual fallback. Recommended starting points:
|
|
7096
|
+
<a href="https://ollama.com/library/qwen2.5-coder" target="_blank" style="color: var(--accent);">qwen2.5-coder</a> (scripts/text) and
|
|
7097
|
+
<a href="https://ollama.com/library/qwen2.5vl" target="_blank" style="color: var(--accent);">qwen2.5vl</a> or
|
|
7098
|
+
<a href="https://ollama.com/library/gemma3" target="_blank" style="color: var(--accent);">gemma3</a> (for example <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code>) for screenshots/vision. You must download and test on your machine.
|
|
7099
|
+
</div>
|
|
7100
|
+
</div>
|
|
7101
|
+
<div class="setting-item">
|
|
7102
|
+
<label class="setting-label" style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
|
7103
|
+
<span>Prefer Ollama Vision for screenshot action detection</span>
|
|
7104
|
+
<input type="checkbox" id="preferOllamaVisionForActionDetection" style="cursor: pointer;">
|
|
7105
|
+
</label>
|
|
7106
|
+
<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px;">
|
|
7107
|
+
When enabled, visual action detection (screenshots → Maestro YAML) tries your Ollama vision model before Claude CLI Vision.
|
|
7051
7108
|
</div>
|
|
7052
7109
|
</div>
|
|
7053
7110
|
<div class="setting-item" id="ollamaModelContainer">
|
|
7054
7111
|
<label class="setting-label" style="display: flex; align-items: center; justify-content: space-between;">
|
|
7055
|
-
<span>Ollama
|
|
7112
|
+
<span>Ollama Models (Text + Vision)</span>
|
|
7056
7113
|
<span id="ollamaStatusBadge" style="font-size: 10px; padding: 2px 6px; border-radius: 4px; background: var(--bg-tertiary); color: var(--text-muted);">Checking...</span>
|
|
7057
7114
|
</label>
|
|
7058
7115
|
<div id="ollamaModelSelector">
|
|
@@ -8074,7 +8131,8 @@
|
|
|
8074
8131
|
}
|
|
8075
8132
|
|
|
8076
8133
|
// LLM Settings
|
|
8077
|
-
let
|
|
8134
|
+
let currentOllamaTextModel = 'qwen2.5-coder:7b';
|
|
8135
|
+
let currentOllamaVisionModel = 'qwen2.5vl:7b';
|
|
8078
8136
|
const LLM_AUTO_PROVIDER_PRIORITY = ['anthropic', 'openai', 'ollama', 'claude-cli'];
|
|
8079
8137
|
|
|
8080
8138
|
function normalizeOllamaModelName(value) {
|
|
@@ -8089,6 +8147,12 @@
|
|
|
8089
8147
|
return installed.split(':')[0] === selected.split(':')[0];
|
|
8090
8148
|
}
|
|
8091
8149
|
|
|
8150
|
+
function isLikelyVisionCapableOllamaModelName(modelName) {
|
|
8151
|
+
const normalized = normalizeOllamaModelName(modelName);
|
|
8152
|
+
if (!normalized) return false;
|
|
8153
|
+
return /(vision|vl\b|llava|bakllava|moondream|gemma3|minicpm-v|qwen2\.5vl|qwen2-vl)/.test(normalized);
|
|
8154
|
+
}
|
|
8155
|
+
|
|
8092
8156
|
function resolveLLMProviderStatus(data) {
|
|
8093
8157
|
const providers = Array.isArray(data?.providers) ? data.providers : [];
|
|
8094
8158
|
const preferredProvider = data?.preferredProvider || 'auto';
|
|
@@ -8125,7 +8189,12 @@
|
|
|
8125
8189
|
document.getElementById('openaiModel').value = data.openaiModel || 'gpt-5.2';
|
|
8126
8190
|
document.getElementById('ollamaUrl').value = data.ollamaUrl || 'http://localhost:11434';
|
|
8127
8191
|
document.getElementById('claudeCliModel').value = data.claudeCliModel || 'haiku';
|
|
8128
|
-
|
|
8192
|
+
currentOllamaTextModel = data.ollamaModel || 'qwen2.5-coder:7b';
|
|
8193
|
+
currentOllamaVisionModel = data.ollamaVisionModel || 'qwen2.5vl:7b';
|
|
8194
|
+
const preferOllamaVisionCheckbox = document.getElementById('preferOllamaVisionForActionDetection');
|
|
8195
|
+
if (preferOllamaVisionCheckbox) {
|
|
8196
|
+
preferOllamaVisionCheckbox.checked = data.preferOllamaVisionForActionDetection === true;
|
|
8197
|
+
}
|
|
8129
8198
|
|
|
8130
8199
|
// Update preferred provider dropdown
|
|
8131
8200
|
await updatePreferredProviderDropdown(data.preferredProvider || 'auto');
|
|
@@ -8171,6 +8240,11 @@
|
|
|
8171
8240
|
const refreshIcon = document.getElementById('ollamaRefreshIcon');
|
|
8172
8241
|
const ollamaUrlInput = document.getElementById('ollamaUrl');
|
|
8173
8242
|
const typedOllamaUrl = (ollamaUrlInput?.value || '').trim();
|
|
8243
|
+
const ollamaTextModelInput = document.getElementById('ollamaTextModel');
|
|
8244
|
+
const ollamaVisionModelInput = document.getElementById('ollamaVisionModel');
|
|
8245
|
+
const preferOllamaVisionForActionDetection = document.getElementById('preferOllamaVisionForActionDetection')?.checked === true;
|
|
8246
|
+
const typedOllamaTextModel = (ollamaTextModelInput?.value || currentOllamaTextModel || 'qwen2.5-coder:7b').trim();
|
|
8247
|
+
const typedOllamaVisionModel = (ollamaVisionModelInput?.value || currentOllamaVisionModel || 'qwen2.5vl:7b').trim();
|
|
8174
8248
|
|
|
8175
8249
|
// Show loading state
|
|
8176
8250
|
if (refreshIcon) {
|
|
@@ -8194,7 +8268,11 @@
|
|
|
8194
8268
|
}
|
|
8195
8269
|
|
|
8196
8270
|
try {
|
|
8197
|
-
const
|
|
8271
|
+
const params = new URLSearchParams();
|
|
8272
|
+
if (typedOllamaUrl) params.set('url', typedOllamaUrl);
|
|
8273
|
+
if (typedOllamaTextModel) params.set('textModel', typedOllamaTextModel);
|
|
8274
|
+
if (typedOllamaVisionModel) params.set('visionModel', typedOllamaVisionModel);
|
|
8275
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
8198
8276
|
const response = await fetch(`/api/ollama/status${query}`);
|
|
8199
8277
|
const data = await response.json();
|
|
8200
8278
|
|
|
@@ -8203,39 +8281,117 @@
|
|
|
8203
8281
|
}
|
|
8204
8282
|
|
|
8205
8283
|
if (data.running && data.models && data.models.length > 0) {
|
|
8206
|
-
const
|
|
8207
|
-
const
|
|
8284
|
+
const selectedTextModelAvailable = data.selectedTextModelAvailable !== false;
|
|
8285
|
+
const selectedVisionModelAvailable = data.selectedVisionModelAvailable !== false;
|
|
8286
|
+
const selectedTextModel = data.currentTextModel || currentOllamaTextModel || 'qwen2.5-coder:7b';
|
|
8287
|
+
const selectedVisionModel = data.currentVisionModel || currentOllamaVisionModel || 'qwen2.5vl:7b';
|
|
8288
|
+
const visionModelLooksCapable = data.selectedVisionModelLooksCapable !== false;
|
|
8208
8289
|
|
|
8209
8290
|
// Ollama is running and has models
|
|
8210
8291
|
if (badge) {
|
|
8211
|
-
if (
|
|
8292
|
+
if (selectedTextModelAvailable && selectedVisionModelAvailable) {
|
|
8212
8293
|
badge.textContent = `${data.models.length} model${data.models.length > 1 ? 's' : ''}`;
|
|
8213
8294
|
badge.style.background = 'rgba(34, 197, 94, 0.15)';
|
|
8214
8295
|
badge.style.color = 'var(--success)';
|
|
8215
8296
|
} else {
|
|
8216
|
-
|
|
8297
|
+
const missing = [
|
|
8298
|
+
selectedTextModelAvailable ? null : 'Text missing',
|
|
8299
|
+
selectedVisionModelAvailable ? null : 'Vision missing'
|
|
8300
|
+
].filter(Boolean).join(' • ') || 'Model missing';
|
|
8301
|
+
badge.textContent = missing;
|
|
8217
8302
|
badge.style.background = 'rgba(245, 158, 11, 0.15)';
|
|
8218
8303
|
badge.style.color = 'var(--warning)';
|
|
8219
8304
|
}
|
|
8220
8305
|
}
|
|
8221
8306
|
if (container) {
|
|
8222
|
-
const
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8307
|
+
const allModels = Array.isArray(data.models) ? data.models : [];
|
|
8308
|
+
const visionCapableModels = allModels.filter(m => isLikelyVisionCapableOllamaModelName(m.name));
|
|
8309
|
+
const nonVisionModels = allModels.filter(m => !isLikelyVisionCapableOllamaModelName(m.name));
|
|
8310
|
+
const visionOptionsModels = preferOllamaVisionForActionDetection
|
|
8311
|
+
? [...visionCapableModels, ...nonVisionModels]
|
|
8312
|
+
: allModels;
|
|
8313
|
+
|
|
8314
|
+
const renderModelOptions = (selectedModelName, modelsList, options = {}) => {
|
|
8315
|
+
const {
|
|
8316
|
+
preserveMissing = true,
|
|
8317
|
+
emptyLabel = 'No compatible models installed',
|
|
8318
|
+
markNonVision = false
|
|
8319
|
+
} = options;
|
|
8320
|
+
const safeModels = Array.isArray(modelsList) ? modelsList : [];
|
|
8321
|
+
if (safeModels.length === 0) {
|
|
8322
|
+
const missingOption = (preserveMissing && selectedModelName)
|
|
8323
|
+
? `<option value="${selectedModelName}" selected>${selectedModelName} (saved, not installed)</option>`
|
|
8324
|
+
: '';
|
|
8325
|
+
return `${missingOption}<option value="" disabled>${emptyLabel}</option>`;
|
|
8326
|
+
}
|
|
8327
|
+
const optionsHtml = safeModels.map(m => {
|
|
8328
|
+
const sizeMB = Math.round(m.size / 1024 / 1024);
|
|
8329
|
+
const sizeStr = sizeMB > 1024 ? `${(sizeMB / 1024).toFixed(1)}GB` : `${sizeMB}MB`;
|
|
8330
|
+
const selected = ollamaModelNamesMatch(m.name, selectedModelName) ? 'selected' : '';
|
|
8331
|
+
const nonVisionSuffix = markNonVision && !isLikelyVisionCapableOllamaModelName(m.name) ? ' • non-vision' : '';
|
|
8332
|
+
return `<option value="${m.name}" ${selected}>${m.name} (${sizeStr})${nonVisionSuffix}</option>`;
|
|
8333
|
+
}).join('');
|
|
8334
|
+
const hasSelected = safeModels.some(m => ollamaModelNamesMatch(m.name, selectedModelName));
|
|
8335
|
+
if (!hasSelected && selectedModelName && preserveMissing) {
|
|
8336
|
+
return `<option value="${selectedModelName}" selected>${selectedModelName} (saved, not installed)</option>${optionsHtml}`;
|
|
8337
|
+
}
|
|
8338
|
+
return optionsHtml;
|
|
8339
|
+
};
|
|
8228
8340
|
|
|
8229
8341
|
container.innerHTML = `
|
|
8230
8342
|
<div style="display: grid; gap: 8px;">
|
|
8231
|
-
<
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8343
|
+
<div style="display: grid; gap: 4px;">
|
|
8344
|
+
<label style="font-size: 11px; color: var(--text-secondary);">Text / Coding Model (chat, summary, scripts)</label>
|
|
8345
|
+
<select class="setting-input" id="ollamaTextModel" style="cursor: pointer;">
|
|
8346
|
+
${renderModelOptions(selectedTextModel, allModels, { emptyLabel: 'No models installed' })}
|
|
8347
|
+
</select>
|
|
8348
|
+
</div>
|
|
8349
|
+
<div style="display: grid; gap: 4px;">
|
|
8350
|
+
<label style="font-size: 11px; color: var(--text-secondary);">Vision Model (screenshots → actions/YAML)</label>
|
|
8351
|
+
<select class="setting-input" id="ollamaVisionModel" style="cursor: pointer;">
|
|
8352
|
+
${renderModelOptions(selectedVisionModel, visionOptionsModels, {
|
|
8353
|
+
emptyLabel: preferOllamaVisionForActionDetection
|
|
8354
|
+
? 'No vision-capable models installed'
|
|
8355
|
+
: 'No models installed',
|
|
8356
|
+
markNonVision: preferOllamaVisionForActionDetection
|
|
8357
|
+
})}
|
|
8358
|
+
</select>
|
|
8359
|
+
</div>
|
|
8360
|
+
<div style="display: grid; gap: 4px;">
|
|
8361
|
+
<label style="font-size: 11px; color: var(--text-secondary);">Available Vision Models (detected)</label>
|
|
8362
|
+
<select class="setting-input" id="ollamaAvailableVisionModelQuickPick" style="cursor: pointer;">
|
|
8363
|
+
${visionCapableModels.length > 0
|
|
8364
|
+
? renderModelOptions(selectedVisionModel, visionCapableModels, {
|
|
8365
|
+
preserveMissing: false,
|
|
8366
|
+
emptyLabel: 'No vision-capable models installed'
|
|
8367
|
+
})
|
|
8368
|
+
: '<option value=\"\" disabled selected>No vision-capable models installed</option>'}
|
|
8369
|
+
</select>
|
|
8370
|
+
</div>
|
|
8371
|
+
<div style="font-size: 10px; color: var(--text-muted); line-height: 1.4;">
|
|
8372
|
+
Recommended: <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5-coder:7b</code> for scripts/text and
|
|
8373
|
+
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code> for screenshots/vision.
|
|
8374
|
+
</div>
|
|
8375
|
+
${selectedTextModelAvailable ? '' : `
|
|
8235
8376
|
<div style="padding: 8px; background: rgba(245, 158, 11, 0.08); border: 1px solid rgba(245, 158, 11, 0.2); border-radius: 6px; color: var(--warning); font-size: 11px;">
|
|
8236
|
-
|
|
8377
|
+
Text model "${selectedTextModel}" is not installed. Choose an available model and save.
|
|
8237
8378
|
</div>
|
|
8238
8379
|
`}
|
|
8380
|
+
${selectedVisionModelAvailable ? '' : `
|
|
8381
|
+
<div style="padding: 8px; background: rgba(245, 158, 11, 0.08); border: 1px solid rgba(245, 158, 11, 0.2); border-radius: 6px; color: var(--warning); font-size: 11px;">
|
|
8382
|
+
Vision model "${selectedVisionModel}" is not installed. Choose an available model and save.
|
|
8383
|
+
</div>
|
|
8384
|
+
`}
|
|
8385
|
+
${selectedVisionModelAvailable && !visionModelLooksCapable ? `
|
|
8386
|
+
<div style="padding: 8px; background: rgba(245, 158, 11, 0.08); border: 1px solid rgba(245, 158, 11, 0.2); border-radius: 6px; color: var(--warning); font-size: 11px;">
|
|
8387
|
+
Vision model "${selectedVisionModel}" may not support images well. A multimodal model like <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code> is recommended.
|
|
8388
|
+
</div>
|
|
8389
|
+
` : ''}
|
|
8390
|
+
${preferOllamaVisionForActionDetection && visionCapableModels.length === 0 ? `
|
|
8391
|
+
<div style="padding: 8px; background: rgba(239, 68, 68, 0.08); border: 1px solid rgba(239, 68, 68, 0.18); border-radius: 6px; color: var(--error); font-size: 11px;">
|
|
8392
|
+
"Prefer Ollama Vision" is enabled, but no installed Ollama models look multimodal/vision-capable. Install <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">qwen2.5vl:7b</code> or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">gemma3:4b</code> (or another vision model).
|
|
8393
|
+
</div>
|
|
8394
|
+
` : ''}
|
|
8239
8395
|
</div>
|
|
8240
8396
|
`;
|
|
8241
8397
|
}
|
|
@@ -8251,7 +8407,9 @@
|
|
|
8251
8407
|
<div style="padding: 10px; background: var(--bg-tertiary); border-radius: 6px; font-size: 12px;">
|
|
8252
8408
|
<div style="color: var(--warning); margin-bottom: 6px;">No models installed</div>
|
|
8253
8409
|
<div style="color: var(--text-muted);">
|
|
8254
|
-
Install
|
|
8410
|
+
Install and test models (examples):
|
|
8411
|
+
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">ollama pull qwen2.5-coder:7b</code> and
|
|
8412
|
+
<code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">ollama pull qwen2.5vl:7b</code> (or <code style="background: var(--bg-elevated); padding: 1px 4px; border-radius: 3px;">ollama pull gemma3:4b</code>)
|
|
8255
8413
|
</div>
|
|
8256
8414
|
</div>
|
|
8257
8415
|
`;
|
|
@@ -8297,9 +8455,12 @@
|
|
|
8297
8455
|
async function saveLLMSettings(options = {}) {
|
|
8298
8456
|
const { silent = false } = options;
|
|
8299
8457
|
try {
|
|
8300
|
-
// Get Ollama
|
|
8301
|
-
const
|
|
8302
|
-
const
|
|
8458
|
+
// Get Ollama models from selects or use current values
|
|
8459
|
+
const ollamaTextModelSelect = document.getElementById('ollamaTextModel');
|
|
8460
|
+
const ollamaVisionModelSelect = document.getElementById('ollamaVisionModel');
|
|
8461
|
+
const ollamaModel = ollamaTextModelSelect?.value || currentOllamaTextModel;
|
|
8462
|
+
const ollamaVisionModel = ollamaVisionModelSelect?.value || currentOllamaVisionModel;
|
|
8463
|
+
const preferOllamaVisionForActionDetection = document.getElementById('preferOllamaVisionForActionDetection')?.checked === true;
|
|
8303
8464
|
|
|
8304
8465
|
const settings = {
|
|
8305
8466
|
anthropicApiKey: document.getElementById('anthropicApiKey')?.value || '',
|
|
@@ -8309,6 +8470,8 @@
|
|
|
8309
8470
|
claudeCliModel: document.getElementById('claudeCliModel')?.value || 'haiku',
|
|
8310
8471
|
ollamaUrl: document.getElementById('ollamaUrl')?.value || 'http://localhost:11434',
|
|
8311
8472
|
ollamaModel: ollamaModel,
|
|
8473
|
+
ollamaVisionModel: ollamaVisionModel,
|
|
8474
|
+
preferOllamaVisionForActionDetection,
|
|
8312
8475
|
preferredProvider: document.getElementById('preferredProvider')?.value || 'auto'
|
|
8313
8476
|
};
|
|
8314
8477
|
|
|
@@ -8327,7 +8490,8 @@
|
|
|
8327
8490
|
}
|
|
8328
8491
|
|
|
8329
8492
|
// Update current model
|
|
8330
|
-
|
|
8493
|
+
currentOllamaTextModel = ollamaModel;
|
|
8494
|
+
currentOllamaVisionModel = ollamaVisionModel;
|
|
8331
8495
|
|
|
8332
8496
|
await checkOllamaStatus();
|
|
8333
8497
|
await updatePreferredProviderDropdown(settings.preferredProvider || 'auto');
|
|
@@ -8421,13 +8585,22 @@
|
|
|
8421
8585
|
document.getElementById('closeSettings').addEventListener('click', closeAllModals);
|
|
8422
8586
|
document.getElementById('refreshSetup').addEventListener('click', loadSetupStatus);
|
|
8423
8587
|
document.getElementById('saveSettings')?.addEventListener('click', saveSettingsFromForm);
|
|
8424
|
-
['preferredProvider', 'anthropicModel', 'openaiModel', 'claudeCliModel', 'ollamaUrl', 'anthropicApiKey', 'openaiApiKey'].forEach((id) => {
|
|
8588
|
+
['preferredProvider', 'anthropicModel', 'openaiModel', 'claudeCliModel', 'ollamaUrl', 'anthropicApiKey', 'openaiApiKey', 'preferOllamaVisionForActionDetection'].forEach((id) => {
|
|
8425
8589
|
document.getElementById(id)?.addEventListener('change', () => {
|
|
8426
8590
|
void saveLLMSettings({ silent: true });
|
|
8427
8591
|
});
|
|
8428
8592
|
});
|
|
8429
8593
|
document.getElementById('ollamaModelSelector')?.addEventListener('change', (event) => {
|
|
8430
|
-
if (event.target && event.target.id === '
|
|
8594
|
+
if (event.target && event.target.id === 'ollamaAvailableVisionModelQuickPick') {
|
|
8595
|
+
const quickPickValue = event.target.value;
|
|
8596
|
+
const visionSelect = document.getElementById('ollamaVisionModel');
|
|
8597
|
+
if (visionSelect && quickPickValue) {
|
|
8598
|
+
visionSelect.value = quickPickValue;
|
|
8599
|
+
}
|
|
8600
|
+
void saveLLMSettings({ silent: true });
|
|
8601
|
+
return;
|
|
8602
|
+
}
|
|
8603
|
+
if (event.target && (event.target.id === 'ollamaTextModel' || event.target.id === 'ollamaVisionModel')) {
|
|
8431
8604
|
void saveLLMSettings({ silent: true });
|
|
8432
8605
|
}
|
|
8433
8606
|
});
|
|
@@ -14733,7 +14906,7 @@
|
|
|
14733
14906
|
|
|
14734
14907
|
// If we have the session locally, generate code directly
|
|
14735
14908
|
if (recorderSession.actions && recorderSession.actions.length > 0) {
|
|
14736
|
-
showCodeModal(generateSpecCode(recorderSession));
|
|
14909
|
+
showCodeModal(generateSpecCode(recorderSession), 'typescript', recorderSession.id, 'web');
|
|
14737
14910
|
return;
|
|
14738
14911
|
}
|
|
14739
14912
|
|
|
@@ -14752,7 +14925,7 @@
|
|
|
14752
14925
|
}
|
|
14753
14926
|
|
|
14754
14927
|
// Show code in a modal
|
|
14755
|
-
showCodeModal(data.specCode || generateSpecCode(data.recording));
|
|
14928
|
+
showCodeModal(data.specCode || generateSpecCode(data.recording), 'typescript', recorderSession.id, 'web');
|
|
14756
14929
|
|
|
14757
14930
|
} catch (error) {
|
|
14758
14931
|
showToast('Failed to load spec code', 'error');
|
|
@@ -16294,20 +16467,557 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16294
16467
|
}
|
|
16295
16468
|
}
|
|
16296
16469
|
|
|
16470
|
+
let codeModalState = null;
|
|
16471
|
+
let codeModalScrollLock = null;
|
|
16472
|
+
const CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY = 'discoverylab.codeModal.mobileRunDeviceId';
|
|
16473
|
+
|
|
16474
|
+
function lockPageScrollForCodeModal() {
|
|
16475
|
+
if (codeModalScrollLock) return;
|
|
16476
|
+
const body = document.body;
|
|
16477
|
+
const docEl = document.documentElement;
|
|
16478
|
+
const scrollX = window.scrollX || docEl.scrollLeft || 0;
|
|
16479
|
+
const scrollY = window.scrollY || docEl.scrollTop || 0;
|
|
16480
|
+
|
|
16481
|
+
codeModalScrollLock = {
|
|
16482
|
+
scrollX,
|
|
16483
|
+
scrollY,
|
|
16484
|
+
body: {
|
|
16485
|
+
position: body.style.position,
|
|
16486
|
+
top: body.style.top,
|
|
16487
|
+
left: body.style.left,
|
|
16488
|
+
right: body.style.right,
|
|
16489
|
+
width: body.style.width,
|
|
16490
|
+
overflow: body.style.overflow
|
|
16491
|
+
},
|
|
16492
|
+
htmlOverflow: docEl.style.overflow
|
|
16493
|
+
};
|
|
16494
|
+
|
|
16495
|
+
docEl.style.overflow = 'hidden';
|
|
16496
|
+
body.style.overflow = 'hidden';
|
|
16497
|
+
body.style.position = 'fixed';
|
|
16498
|
+
body.style.top = `-${scrollY}px`;
|
|
16499
|
+
body.style.left = `-${scrollX}px`;
|
|
16500
|
+
body.style.right = '0';
|
|
16501
|
+
body.style.width = '100%';
|
|
16502
|
+
}
|
|
16503
|
+
|
|
16504
|
+
function unlockPageScrollForCodeModal() {
|
|
16505
|
+
if (!codeModalScrollLock) return;
|
|
16506
|
+
const body = document.body;
|
|
16507
|
+
const docEl = document.documentElement;
|
|
16508
|
+
const lock = codeModalScrollLock;
|
|
16509
|
+
|
|
16510
|
+
docEl.style.overflow = lock.htmlOverflow || '';
|
|
16511
|
+
body.style.position = lock.body.position || '';
|
|
16512
|
+
body.style.top = lock.body.top || '';
|
|
16513
|
+
body.style.left = lock.body.left || '';
|
|
16514
|
+
body.style.right = lock.body.right || '';
|
|
16515
|
+
body.style.width = lock.body.width || '';
|
|
16516
|
+
body.style.overflow = lock.body.overflow || '';
|
|
16517
|
+
|
|
16518
|
+
window.scrollTo(lock.scrollX || 0, lock.scrollY || 0);
|
|
16519
|
+
codeModalScrollLock = null;
|
|
16520
|
+
}
|
|
16521
|
+
|
|
16522
|
+
function getPreferredCodeModalMobileDeviceId() {
|
|
16523
|
+
if (codeModalState?.mobileRunSelectedDeviceId) return codeModalState.mobileRunSelectedDeviceId;
|
|
16524
|
+
try {
|
|
16525
|
+
const fromStorage = localStorage.getItem(CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY);
|
|
16526
|
+
if (fromStorage) return fromStorage;
|
|
16527
|
+
} catch {}
|
|
16528
|
+
const deviceDropdown = document.getElementById('deviceDropdown');
|
|
16529
|
+
if (deviceDropdown && deviceDropdown.value) return deviceDropdown.value;
|
|
16530
|
+
return '';
|
|
16531
|
+
}
|
|
16532
|
+
|
|
16533
|
+
function formatCodeModalMobileDeviceOptionLabel(device) {
|
|
16534
|
+
const platform = String(device?.platform || '').toUpperCase() || 'MOBILE';
|
|
16535
|
+
const name = String(device?.name || device?.id || 'Unknown device');
|
|
16536
|
+
const status = String(device?.status || '').toLowerCase();
|
|
16537
|
+
return status ? `${platform} · ${name} (${status})` : `${platform} · ${name}`;
|
|
16538
|
+
}
|
|
16539
|
+
|
|
16540
|
+
function renderCodeModalMobileDevicePicker() {
|
|
16541
|
+
const selectEl = document.getElementById('codeModalMobileDeviceSelect');
|
|
16542
|
+
const statusEl = document.getElementById('codeModalMobileDeviceStatus');
|
|
16543
|
+
if (!selectEl || !statusEl || !codeModalState) return;
|
|
16544
|
+
|
|
16545
|
+
const devices = Array.isArray(codeModalState.mobileRunDevices) ? codeModalState.mobileRunDevices : [];
|
|
16546
|
+
const loading = codeModalState.mobileRunDevicesLoading === true;
|
|
16547
|
+
const selectedId = codeModalState.mobileRunSelectedDeviceId || '';
|
|
16548
|
+
|
|
16549
|
+
const options = ['<option value="">Auto-detect device (Maestro default)</option>'];
|
|
16550
|
+
for (const device of devices) {
|
|
16551
|
+
const id = String(device?.id || '').trim();
|
|
16552
|
+
if (!id) continue;
|
|
16553
|
+
const selectedAttr = selectedId === id ? ' selected' : '';
|
|
16554
|
+
options.push(`<option value="${escapeAttr(id)}"${selectedAttr}>${escapeHtml(formatCodeModalMobileDeviceOptionLabel(device))}</option>`);
|
|
16555
|
+
}
|
|
16556
|
+
selectEl.innerHTML = options.join('');
|
|
16557
|
+
|
|
16558
|
+
if (loading) {
|
|
16559
|
+
statusEl.textContent = 'Loading devices...';
|
|
16560
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
16561
|
+
} else if (devices.length === 0) {
|
|
16562
|
+
statusEl.textContent = 'No connected/booted devices found';
|
|
16563
|
+
statusEl.style.color = 'var(--warning)';
|
|
16564
|
+
} else if (selectedId) {
|
|
16565
|
+
const selected = devices.find(d => String(d.id || '') === selectedId);
|
|
16566
|
+
statusEl.textContent = selected ? `Selected: ${selected.name}` : 'Selected device unavailable (using auto)';
|
|
16567
|
+
statusEl.style.color = selected ? 'var(--success)' : 'var(--warning)';
|
|
16568
|
+
} else {
|
|
16569
|
+
statusEl.textContent = 'Using auto-detect unless a device is selected';
|
|
16570
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
16571
|
+
}
|
|
16572
|
+
}
|
|
16573
|
+
|
|
16574
|
+
async function loadCodeModalMobileRunDevices(forceRefresh = false) {
|
|
16575
|
+
if (!codeModalState || codeModalState.type !== 'mobile') return;
|
|
16576
|
+
codeModalState.mobileRunDevicesLoading = true;
|
|
16577
|
+
renderCodeModalMobileDevicePicker();
|
|
16578
|
+
try {
|
|
16579
|
+
const url = forceRefresh
|
|
16580
|
+
? '/api/testing/mobile/maestro-devices?refresh=1'
|
|
16581
|
+
: '/api/testing/mobile/maestro-devices';
|
|
16582
|
+
const response = await fetch(url);
|
|
16583
|
+
const data = await response.json();
|
|
16584
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load devices');
|
|
16585
|
+
const devices = Array.isArray(data.devices) ? data.devices : [];
|
|
16586
|
+
codeModalState.mobileRunDevices = devices;
|
|
16587
|
+
|
|
16588
|
+
const preferred = codeModalState.mobileRunSelectedDeviceId || getPreferredCodeModalMobileDeviceId();
|
|
16589
|
+
if (preferred && devices.some(d => String(d.id || '') === preferred)) {
|
|
16590
|
+
codeModalState.mobileRunSelectedDeviceId = preferred;
|
|
16591
|
+
} else {
|
|
16592
|
+
codeModalState.mobileRunSelectedDeviceId = '';
|
|
16593
|
+
}
|
|
16594
|
+
renderCodeModalMobileDevicePicker();
|
|
16595
|
+
} catch (error) {
|
|
16596
|
+
console.error('Failed to load Maestro devices for code modal:', error);
|
|
16597
|
+
codeModalState.mobileRunDevices = [];
|
|
16598
|
+
codeModalState.mobileRunSelectedDeviceId = '';
|
|
16599
|
+
renderCodeModalMobileDevicePicker();
|
|
16600
|
+
const statusEl = document.getElementById('codeModalMobileDeviceStatus');
|
|
16601
|
+
if (statusEl) {
|
|
16602
|
+
statusEl.textContent = `Failed to load devices: ${error.message || 'Unknown error'}`;
|
|
16603
|
+
statusEl.style.color = 'var(--error)';
|
|
16604
|
+
}
|
|
16605
|
+
} finally {
|
|
16606
|
+
if (codeModalState) codeModalState.mobileRunDevicesLoading = false;
|
|
16607
|
+
renderCodeModalMobileDevicePicker();
|
|
16608
|
+
}
|
|
16609
|
+
}
|
|
16610
|
+
|
|
16611
|
+
function onCodeModalMobileDeviceChanged(value) {
|
|
16612
|
+
if (!codeModalState) return;
|
|
16613
|
+
const normalized = String(value || '').trim();
|
|
16614
|
+
codeModalState.mobileRunSelectedDeviceId = normalized;
|
|
16615
|
+
try {
|
|
16616
|
+
if (normalized) localStorage.setItem(CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY, normalized);
|
|
16617
|
+
else localStorage.removeItem(CODE_MODAL_MOBILE_DEVICE_STORAGE_KEY);
|
|
16618
|
+
} catch {}
|
|
16619
|
+
renderCodeModalMobileDevicePicker();
|
|
16620
|
+
}
|
|
16621
|
+
|
|
16622
|
+
function findScrollableAncestorInModal(target, modalRoot) {
|
|
16623
|
+
let el = target instanceof Element ? target : null;
|
|
16624
|
+
while (el && el !== modalRoot) {
|
|
16625
|
+
const style = window.getComputedStyle(el);
|
|
16626
|
+
const overflowY = style.overflowY || style.overflow;
|
|
16627
|
+
const overflowX = style.overflowX || style.overflow;
|
|
16628
|
+
const canScrollY = /(auto|scroll)/.test(overflowY) && el.scrollHeight > (el.clientHeight + 1);
|
|
16629
|
+
const canScrollX = /(auto|scroll)/.test(overflowX) && el.scrollWidth > (el.clientWidth + 1);
|
|
16630
|
+
if (canScrollY || canScrollX) return el;
|
|
16631
|
+
el = el.parentElement;
|
|
16632
|
+
}
|
|
16633
|
+
return null;
|
|
16634
|
+
}
|
|
16635
|
+
|
|
16636
|
+
function attachCodeModalScrollTrap(modalEl) {
|
|
16637
|
+
if (!modalEl) return;
|
|
16638
|
+
const wheelHandler = (e) => {
|
|
16639
|
+
if (!modalEl.contains(e.target)) return;
|
|
16640
|
+
const path = typeof e.composedPath === 'function' ? e.composedPath() : [];
|
|
16641
|
+
const firstElementInPath = path.find((node) => node instanceof Element);
|
|
16642
|
+
const targetEl = (e.target instanceof Element ? e.target : null) || (firstElementInPath instanceof Element ? firstElementInPath : null);
|
|
16643
|
+
|
|
16644
|
+
// Only intercept wheel when cursor is on the line-number gutter.
|
|
16645
|
+
// Everything else should use native scrolling in Chrome (textarea/vars/output).
|
|
16646
|
+
if (targetEl?.closest('#codeLineNumbers')) {
|
|
16647
|
+
const textarea = modalEl.querySelector('#codeEditorTextarea');
|
|
16648
|
+
if (textarea) {
|
|
16649
|
+
textarea.scrollBy({
|
|
16650
|
+
top: e.deltaY || 0,
|
|
16651
|
+
left: e.deltaX || 0,
|
|
16652
|
+
behavior: 'auto'
|
|
16653
|
+
});
|
|
16654
|
+
}
|
|
16655
|
+
e.preventDefault();
|
|
16656
|
+
e.stopPropagation();
|
|
16657
|
+
return;
|
|
16658
|
+
}
|
|
16659
|
+
};
|
|
16660
|
+
|
|
16661
|
+
modalEl.addEventListener('wheel', wheelHandler, { passive: false, capture: true });
|
|
16662
|
+
modalEl._wheelTrapHandler = wheelHandler;
|
|
16663
|
+
}
|
|
16664
|
+
|
|
16665
|
+
function getCodeModalOwnerType(type) {
|
|
16666
|
+
return type === 'mobile' ? 'mobile-recording' : 'web-recording';
|
|
16667
|
+
}
|
|
16668
|
+
|
|
16669
|
+
function extractCodeModalPlaceholders(code) {
|
|
16670
|
+
const matches = new Set();
|
|
16671
|
+
const regex = /\$\{([A-Z][A-Z0-9_]*)\}/g;
|
|
16672
|
+
let match;
|
|
16673
|
+
const text = String(code || '');
|
|
16674
|
+
while ((match = regex.exec(text)) !== null) {
|
|
16675
|
+
if (match[1]) matches.add(match[1]);
|
|
16676
|
+
}
|
|
16677
|
+
return Array.from(matches).sort();
|
|
16678
|
+
}
|
|
16679
|
+
|
|
16680
|
+
function escapeAttr(value) {
|
|
16681
|
+
return String(value ?? '')
|
|
16682
|
+
.replace(/&/g, '&')
|
|
16683
|
+
.replace(/"/g, '"')
|
|
16684
|
+
.replace(/</g, '<')
|
|
16685
|
+
.replace(/>/g, '>');
|
|
16686
|
+
}
|
|
16687
|
+
|
|
16688
|
+
function parseEnvTestTextClient(rawText) {
|
|
16689
|
+
const rows = [];
|
|
16690
|
+
const lines = String(rawText || '').split(/\r?\n/);
|
|
16691
|
+
let pendingNotes = [];
|
|
16692
|
+
let pendingPlatform = 'both';
|
|
16693
|
+
for (const line of lines) {
|
|
16694
|
+
const trimmed = line.trim();
|
|
16695
|
+
if (!trimmed) {
|
|
16696
|
+
pendingNotes = [];
|
|
16697
|
+
pendingPlatform = 'both';
|
|
16698
|
+
continue;
|
|
16699
|
+
}
|
|
16700
|
+
if (trimmed.startsWith('#')) {
|
|
16701
|
+
const comment = trimmed.slice(1).trim();
|
|
16702
|
+
const m = comment.match(/^platform:\s*(mobile|web|both)$/i);
|
|
16703
|
+
if (m) {
|
|
16704
|
+
pendingPlatform = m[1].toLowerCase();
|
|
16705
|
+
} else if (comment) {
|
|
16706
|
+
pendingNotes.push(comment);
|
|
16707
|
+
}
|
|
16708
|
+
continue;
|
|
16709
|
+
}
|
|
16710
|
+
const eqIndex = line.indexOf('=');
|
|
16711
|
+
if (eqIndex <= 0) continue;
|
|
16712
|
+
const key = line.slice(0, eqIndex).trim().toUpperCase();
|
|
16713
|
+
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(key)) continue;
|
|
16714
|
+
rows.push({
|
|
16715
|
+
id: 'var_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
|
|
16716
|
+
key,
|
|
16717
|
+
value: line.slice(eqIndex + 1).replace(/\\n/g, '\n'),
|
|
16718
|
+
isSecret: /PASSWORD|SECRET|TOKEN|KEY|OTP|PIN/.test(key),
|
|
16719
|
+
platform: ['mobile', 'web', 'both'].includes(pendingPlatform) ? pendingPlatform : 'both',
|
|
16720
|
+
notes: pendingNotes.join(' ') || ''
|
|
16721
|
+
});
|
|
16722
|
+
pendingNotes = [];
|
|
16723
|
+
pendingPlatform = 'both';
|
|
16724
|
+
}
|
|
16725
|
+
return rows;
|
|
16726
|
+
}
|
|
16727
|
+
|
|
16728
|
+
function renderEnvTestTextClient(rows) {
|
|
16729
|
+
const lines = [];
|
|
16730
|
+
for (const row of (rows || [])) {
|
|
16731
|
+
const key = String(row.key || '').trim().toUpperCase();
|
|
16732
|
+
if (!key) continue;
|
|
16733
|
+
if (row.notes) lines.push(`# ${String(row.notes).trim()}`);
|
|
16734
|
+
if (row.platform && row.platform !== 'both') lines.push(`# platform: ${row.platform}`);
|
|
16735
|
+
lines.push(`${key}=${String(row.value || '').replace(/\n/g, '\\n')}`);
|
|
16736
|
+
lines.push('');
|
|
16737
|
+
}
|
|
16738
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
16739
|
+
}
|
|
16740
|
+
|
|
16741
|
+
function collectCodeModalVariablesFromTable() {
|
|
16742
|
+
const rows = Array.from(document.querySelectorAll('#codeModalVarsTableBody tr[data-var-row]'));
|
|
16743
|
+
return rows.map((tr) => ({
|
|
16744
|
+
id: tr.dataset.id || '',
|
|
16745
|
+
key: (tr.querySelector('[data-field="key"]')?.value || '').trim().toUpperCase(),
|
|
16746
|
+
value: tr.querySelector('[data-field="value"]')?.value || '',
|
|
16747
|
+
isSecret: tr.querySelector('[data-field="isSecret"]')?.checked === true,
|
|
16748
|
+
platform: tr.querySelector('[data-field="platform"]')?.value || 'both',
|
|
16749
|
+
notes: (tr.querySelector('[data-field="notes"]')?.value || '').trim(),
|
|
16750
|
+
})).filter((row) => row.key);
|
|
16751
|
+
}
|
|
16752
|
+
|
|
16753
|
+
function getCodeModalVariablesSignature(rows) {
|
|
16754
|
+
return JSON.stringify((rows || []).map((row) => ({
|
|
16755
|
+
key: String(row.key || '').trim().toUpperCase(),
|
|
16756
|
+
value: String(row.value || ''),
|
|
16757
|
+
isSecret: row.isSecret === true,
|
|
16758
|
+
platform: ['mobile', 'web', 'both'].includes(row.platform) ? row.platform : 'both',
|
|
16759
|
+
notes: String(row.notes || '').trim(),
|
|
16760
|
+
})));
|
|
16761
|
+
}
|
|
16762
|
+
|
|
16763
|
+
function syncCodeModalEnvFromTable() {
|
|
16764
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16765
|
+
if (!envTextarea) return;
|
|
16766
|
+
const rows = collectCodeModalVariablesFromTable();
|
|
16767
|
+
envTextarea.value = renderEnvTestTextClient(rows);
|
|
16768
|
+
if (codeModalState) codeModalState.variables = rows;
|
|
16769
|
+
updateCodeModalVariablesStatus();
|
|
16770
|
+
}
|
|
16771
|
+
|
|
16772
|
+
function updateCodeModalVariablesStatus() {
|
|
16773
|
+
if (!codeModalState) return;
|
|
16774
|
+
const codeText = document.getElementById('codeEditorTextarea')?.value || codeModalState.code || '';
|
|
16775
|
+
const placeholders = extractCodeModalPlaceholders(codeText);
|
|
16776
|
+
codeModalState.placeholders = placeholders;
|
|
16777
|
+
const rows = collectCodeModalVariablesFromTable();
|
|
16778
|
+
const activePlatform = codeModalState.type === 'mobile' ? 'mobile' : 'web';
|
|
16779
|
+
const scoped = rows.filter((row) => (row.platform || 'both') === 'both' || row.platform === activePlatform);
|
|
16780
|
+
const rowKeys = new Set(scoped.map((row) => String(row.key || '').trim().toUpperCase()).filter(Boolean));
|
|
16781
|
+
const missing = placeholders.filter((key) => !rowKeys.has(key));
|
|
16782
|
+
const unused = scoped.map((row) => row.key).filter((key) => key && !placeholders.includes(key));
|
|
16783
|
+
codeModalState.missingPlaceholders = missing;
|
|
16784
|
+
|
|
16785
|
+
const summaryEl = document.getElementById('codeModalVarSummary');
|
|
16786
|
+
if (summaryEl) {
|
|
16787
|
+
summaryEl.innerHTML = `
|
|
16788
|
+
<span>${placeholders.length} placeholder${placeholders.length === 1 ? '' : 's'} in script</span>
|
|
16789
|
+
<span style="color:${missing.length ? 'var(--warning)' : 'var(--success)'};">${missing.length} missing</span>
|
|
16790
|
+
<span>${unused.length} unused</span>
|
|
16791
|
+
`;
|
|
16792
|
+
}
|
|
16793
|
+
|
|
16794
|
+
const badgeEl = document.getElementById('codeModalVarsTabBadge');
|
|
16795
|
+
if (badgeEl) {
|
|
16796
|
+
badgeEl.textContent = missing.length > 0 ? `${missing.length} missing` : 'Ready';
|
|
16797
|
+
badgeEl.style.color = missing.length > 0 ? 'var(--warning)' : 'var(--success)';
|
|
16798
|
+
}
|
|
16799
|
+
|
|
16800
|
+
const missingEl = document.getElementById('codeModalMissingList');
|
|
16801
|
+
if (missingEl) {
|
|
16802
|
+
missingEl.textContent = missing.length > 0 ? `Missing: ${missing.join(', ')}` : 'All placeholders have values for this platform.';
|
|
16803
|
+
missingEl.style.color = missing.length > 0 ? 'var(--warning)' : 'var(--text-muted)';
|
|
16804
|
+
}
|
|
16805
|
+
}
|
|
16806
|
+
|
|
16807
|
+
function renderCodeModalVariablesTable() {
|
|
16808
|
+
if (!codeModalState) return;
|
|
16809
|
+
const body = document.getElementById('codeModalVarsTableBody');
|
|
16810
|
+
if (!body) return;
|
|
16811
|
+
const rows = Array.isArray(codeModalState.variables) ? codeModalState.variables : [];
|
|
16812
|
+
body.innerHTML = rows.length > 0 ? rows.map((row, index) => `
|
|
16813
|
+
<tr data-var-row data-id="${escapeAttr(row.id || '')}">
|
|
16814
|
+
<td><input data-field="key" class="setting-input" style="min-width: 120px;" value="${escapeAttr(row.key || '')}" placeholder="PASSWORD"></td>
|
|
16815
|
+
<td><input data-field="value" type="${row.isSecret ? 'password' : 'text'}" class="setting-input" style="min-width: 180px;" value="${escapeAttr(row.value || '')}" placeholder="value"></td>
|
|
16816
|
+
<td>
|
|
16817
|
+
<select data-field="platform" class="setting-input" style="min-width: 90px;">
|
|
16818
|
+
<option value="both" ${row.platform === 'both' ? 'selected' : ''}>both</option>
|
|
16819
|
+
<option value="mobile" ${row.platform === 'mobile' ? 'selected' : ''}>mobile</option>
|
|
16820
|
+
<option value="web" ${row.platform === 'web' ? 'selected' : ''}>web</option>
|
|
16821
|
+
</select>
|
|
16822
|
+
</td>
|
|
16823
|
+
<td style="text-align: center;">
|
|
16824
|
+
<input data-field="isSecret" type="checkbox" ${row.isSecret ? 'checked' : ''} onchange="toggleCodeModalVarSecret(${index}, this.checked)">
|
|
16825
|
+
</td>
|
|
16826
|
+
<td><input data-field="notes" class="setting-input" style="min-width: 160px;" value="${escapeAttr(row.notes || '')}" placeholder="optional note"></td>
|
|
16827
|
+
<td style="text-align: right;">
|
|
16828
|
+
<button class="btn btn-secondary" style="padding: 6px 8px;" onclick="removeCodeModalVariableRow(${index})">Remove</button>
|
|
16829
|
+
</td>
|
|
16830
|
+
</tr>
|
|
16831
|
+
`).join('') : `<tr><td colspan="6" style="color: var(--text-muted); padding: 12px; text-align: center;">No variables saved yet</td></tr>`;
|
|
16832
|
+
|
|
16833
|
+
body.querySelectorAll('input, select').forEach((el) => {
|
|
16834
|
+
el.addEventListener('input', () => syncCodeModalEnvFromTable());
|
|
16835
|
+
el.addEventListener('change', () => syncCodeModalEnvFromTable());
|
|
16836
|
+
});
|
|
16837
|
+
updateCodeModalVariablesStatus();
|
|
16838
|
+
}
|
|
16839
|
+
|
|
16840
|
+
function addCodeModalVariableRow(defaults = {}) {
|
|
16841
|
+
if (!codeModalState) return;
|
|
16842
|
+
codeModalState.variables = Array.isArray(codeModalState.variables) ? codeModalState.variables : [];
|
|
16843
|
+
codeModalState.variables.push({
|
|
16844
|
+
id: defaults.id || ('var_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7)),
|
|
16845
|
+
key: (defaults.key || '').toUpperCase(),
|
|
16846
|
+
value: defaults.value || '',
|
|
16847
|
+
isSecret: defaults.isSecret !== false,
|
|
16848
|
+
platform: defaults.platform || 'both',
|
|
16849
|
+
notes: defaults.notes || ''
|
|
16850
|
+
});
|
|
16851
|
+
renderCodeModalVariablesTable();
|
|
16852
|
+
syncCodeModalEnvFromTable();
|
|
16853
|
+
}
|
|
16854
|
+
|
|
16855
|
+
function removeCodeModalVariableRow(index) {
|
|
16856
|
+
if (!codeModalState || !Array.isArray(codeModalState.variables)) return;
|
|
16857
|
+
codeModalState.variables.splice(index, 1);
|
|
16858
|
+
renderCodeModalVariablesTable();
|
|
16859
|
+
syncCodeModalEnvFromTable();
|
|
16860
|
+
}
|
|
16861
|
+
|
|
16862
|
+
function toggleCodeModalVarSecret(index, checked) {
|
|
16863
|
+
if (!codeModalState || !Array.isArray(codeModalState.variables) || !codeModalState.variables[index]) return;
|
|
16864
|
+
codeModalState.variables[index].isSecret = !!checked;
|
|
16865
|
+
renderCodeModalVariablesTable();
|
|
16866
|
+
}
|
|
16867
|
+
|
|
16868
|
+
function addMissingCodeModalVariablesFromScript() {
|
|
16869
|
+
if (!codeModalState) return;
|
|
16870
|
+
const codeText = document.getElementById('codeEditorTextarea')?.value || '';
|
|
16871
|
+
const placeholders = extractCodeModalPlaceholders(codeText);
|
|
16872
|
+
const existing = new Set((codeModalState.variables || []).map((row) => String(row.key || '').toUpperCase()));
|
|
16873
|
+
let added = 0;
|
|
16874
|
+
placeholders.forEach((key) => {
|
|
16875
|
+
if (existing.has(key)) return;
|
|
16876
|
+
addCodeModalVariableRow({
|
|
16877
|
+
key,
|
|
16878
|
+
value: '',
|
|
16879
|
+
isSecret: /PASSWORD|SECRET|TOKEN|KEY|OTP|PIN/.test(key),
|
|
16880
|
+
platform: 'both',
|
|
16881
|
+
notes: ''
|
|
16882
|
+
});
|
|
16883
|
+
existing.add(key);
|
|
16884
|
+
added += 1;
|
|
16885
|
+
});
|
|
16886
|
+
if (added === 0) {
|
|
16887
|
+
showToast('No new placeholders to add', 'info');
|
|
16888
|
+
} else {
|
|
16889
|
+
showToast(`${added} variable${added === 1 ? '' : 's'} added from script`, 'success');
|
|
16890
|
+
}
|
|
16891
|
+
}
|
|
16892
|
+
|
|
16893
|
+
function applyEnvTestToCodeModalTable() {
|
|
16894
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16895
|
+
if (!envTextarea || !codeModalState) return;
|
|
16896
|
+
const parsed = parseEnvTestTextClient(envTextarea.value);
|
|
16897
|
+
const previousByKey = new Map((codeModalState.variables || []).map((row) => [String(row.key || '').toUpperCase(), row]));
|
|
16898
|
+
codeModalState.variables = parsed.map((row) => ({
|
|
16899
|
+
...row,
|
|
16900
|
+
isSecret: previousByKey.get(row.key)?.isSecret ?? row.isSecret,
|
|
16901
|
+
}));
|
|
16902
|
+
renderCodeModalVariablesTable();
|
|
16903
|
+
syncCodeModalEnvFromTable();
|
|
16904
|
+
showToast('Applied .env.test to variables table', 'success');
|
|
16905
|
+
}
|
|
16906
|
+
|
|
16907
|
+
async function loadCodeModalVariables() {
|
|
16908
|
+
if (!codeModalState?.recordingId || !codeModalState?.ownerType) return;
|
|
16909
|
+
try {
|
|
16910
|
+
const response = await fetch(`/api/test-variables/${codeModalState.ownerType}/${codeModalState.recordingId}`);
|
|
16911
|
+
const data = await response.json();
|
|
16912
|
+
if (!response.ok) throw new Error(data.error || 'Failed to load variables');
|
|
16913
|
+
codeModalState.variables = Array.isArray(data.variables) ? data.variables : [];
|
|
16914
|
+
codeModalState.variablesLoaded = true;
|
|
16915
|
+
codeModalState.lastSavedVariablesSignature = getCodeModalVariablesSignature(codeModalState.variables);
|
|
16916
|
+
renderCodeModalVariablesTable();
|
|
16917
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16918
|
+
if (envTextarea) {
|
|
16919
|
+
envTextarea.value = typeof data.envTest === 'string' ? data.envTest : renderEnvTestTextClient(codeModalState.variables);
|
|
16920
|
+
}
|
|
16921
|
+
updateCodeModalVariablesStatus();
|
|
16922
|
+
return true;
|
|
16923
|
+
} catch (error) {
|
|
16924
|
+
console.error('Failed to load code modal variables:', error);
|
|
16925
|
+
const missingEl = document.getElementById('codeModalMissingList');
|
|
16926
|
+
if (missingEl) {
|
|
16927
|
+
missingEl.textContent = `Failed to load variables: ${error.message || 'Unknown error'}`;
|
|
16928
|
+
missingEl.style.color = 'var(--error)';
|
|
16929
|
+
}
|
|
16930
|
+
return false;
|
|
16931
|
+
}
|
|
16932
|
+
}
|
|
16933
|
+
|
|
16934
|
+
async function saveCodeModalVariables(options = {}) {
|
|
16935
|
+
if (!codeModalState?.recordingId || !codeModalState?.ownerType) return false;
|
|
16936
|
+
const { silent = false } = options;
|
|
16937
|
+
try {
|
|
16938
|
+
if (!codeModalState.variablesLoaded) {
|
|
16939
|
+
const loaded = await loadCodeModalVariables();
|
|
16940
|
+
if (!loaded) {
|
|
16941
|
+
throw new Error('Unable to load existing variables');
|
|
16942
|
+
}
|
|
16943
|
+
}
|
|
16944
|
+
const rows = collectCodeModalVariablesFromTable().map((row) => ({
|
|
16945
|
+
...row,
|
|
16946
|
+
key: String(row.key || '').trim().toUpperCase(),
|
|
16947
|
+
platform: ['mobile', 'web', 'both'].includes(row.platform) ? row.platform : 'both'
|
|
16948
|
+
})).filter((row) => row.key);
|
|
16949
|
+
const response = await fetch(`/api/test-variables/${codeModalState.ownerType}/${codeModalState.recordingId}`, {
|
|
16950
|
+
method: 'PUT',
|
|
16951
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16952
|
+
body: JSON.stringify({ variables: rows })
|
|
16953
|
+
});
|
|
16954
|
+
const data = await response.json();
|
|
16955
|
+
if (!response.ok) throw new Error(data.error || 'Failed to save variables');
|
|
16956
|
+
codeModalState.variables = Array.isArray(data.variables) ? data.variables : rows;
|
|
16957
|
+
codeModalState.lastSavedVariablesSignature = getCodeModalVariablesSignature(codeModalState.variables);
|
|
16958
|
+
renderCodeModalVariablesTable();
|
|
16959
|
+
const envTextarea = document.getElementById('codeModalEnvTextarea');
|
|
16960
|
+
if (envTextarea) envTextarea.value = typeof data.envTest === 'string' ? data.envTest : renderEnvTestTextClient(codeModalState.variables);
|
|
16961
|
+
if (!silent) showToast('Test variables saved', 'success');
|
|
16962
|
+
return true;
|
|
16963
|
+
} catch (error) {
|
|
16964
|
+
if (!silent) showToast(`Failed to save variables: ${error.message || 'Unknown error'}`, 'error');
|
|
16965
|
+
return false;
|
|
16966
|
+
}
|
|
16967
|
+
}
|
|
16968
|
+
|
|
16969
|
+
function switchCodeModalTab(tab) {
|
|
16970
|
+
const codePanel = document.getElementById('codeModalCodePanel');
|
|
16971
|
+
const varsPanel = document.getElementById('codeModalVarsPanel');
|
|
16972
|
+
const codeBtn = document.getElementById('codeModalTabCode');
|
|
16973
|
+
const varsBtn = document.getElementById('codeModalTabVars');
|
|
16974
|
+
if (codePanel) codePanel.style.display = tab === 'code' ? 'flex' : 'none';
|
|
16975
|
+
if (varsPanel) varsPanel.style.display = tab === 'vars' ? 'flex' : 'none';
|
|
16976
|
+
if (codeBtn) codeBtn.style.opacity = tab === 'code' ? '1' : '0.7';
|
|
16977
|
+
if (varsBtn) varsBtn.style.opacity = tab === 'vars' ? '1' : '0.7';
|
|
16978
|
+
if (codeModalState) codeModalState.activeTab = tab;
|
|
16979
|
+
if (tab === 'vars') {
|
|
16980
|
+
updateCodeModalVariablesStatus();
|
|
16981
|
+
}
|
|
16982
|
+
}
|
|
16983
|
+
|
|
16297
16984
|
// Generic code modal for both Web (TypeScript) and Mobile (YAML)
|
|
16298
16985
|
function showCodeModal(code, language, recordingId, type) {
|
|
16299
|
-
const
|
|
16986
|
+
const resolvedLanguage = language || 'typescript';
|
|
16987
|
+
const resolvedType = type || (resolvedLanguage === 'yaml' ? 'mobile' : 'web');
|
|
16988
|
+
const resolvedRecordingId = recordingId || (resolvedType === 'web' ? recorderSession?.id : currentProject?.id || lastMobileRecording?.projectId);
|
|
16989
|
+
const isYaml = resolvedLanguage === 'yaml';
|
|
16300
16990
|
const title = isYaml ? 'Maestro Flow (YAML)' : 'Playwright Test (TypeScript)';
|
|
16301
16991
|
const lineCount = (code || '').split('\n').length;
|
|
16302
16992
|
const lineNumbers = Array.from({length: Math.max(lineCount, 20)}, (_, i) => i + 1).join('\n');
|
|
16993
|
+
const hasVarsTab = !!resolvedRecordingId && (resolvedType === 'mobile' || resolvedType === 'web');
|
|
16994
|
+
|
|
16995
|
+
codeModalState = {
|
|
16996
|
+
code: code || '',
|
|
16997
|
+
language: resolvedLanguage,
|
|
16998
|
+
type: resolvedType,
|
|
16999
|
+
recordingId: resolvedRecordingId || null,
|
|
17000
|
+
ownerType: hasVarsTab ? getCodeModalOwnerType(resolvedType) : null,
|
|
17001
|
+
variables: [],
|
|
17002
|
+
variablesLoaded: false,
|
|
17003
|
+
lastSavedVariablesSignature: '',
|
|
17004
|
+
placeholders: extractCodeModalPlaceholders(code || ''),
|
|
17005
|
+
missingPlaceholders: [],
|
|
17006
|
+
activeTab: 'code',
|
|
17007
|
+
mobileRunDevices: [],
|
|
17008
|
+
mobileRunDevicesLoading: resolvedType === 'mobile',
|
|
17009
|
+
mobileRunSelectedDeviceId: resolvedType === 'mobile' ? getPreferredCodeModalMobileDeviceId() : '',
|
|
17010
|
+
lastSavedCode: code || ''
|
|
17011
|
+
};
|
|
16303
17012
|
|
|
16304
17013
|
const existing = document.getElementById('codeModal');
|
|
16305
17014
|
if (existing) {
|
|
16306
|
-
|
|
17015
|
+
closeCodeModal();
|
|
16307
17016
|
}
|
|
16308
17017
|
|
|
16309
17018
|
const modal = document.createElement('div');
|
|
16310
17019
|
modal.className = 'modal-overlay';
|
|
17020
|
+
modal.classList.add('code-modal-overlay');
|
|
16311
17021
|
modal.id = 'codeModal';
|
|
16312
17022
|
modal.innerHTML = `
|
|
16313
17023
|
<div class="modal code-modal">
|
|
@@ -16315,21 +17025,95 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16315
17025
|
<h3>${title}</h3>
|
|
16316
17026
|
<button class="modal-close" onclick="closeCodeModal()">×</button>
|
|
16317
17027
|
</div>
|
|
17028
|
+
<div style="display:flex; gap:8px; padding: 10px 16px 0 16px; border-bottom: 1px solid var(--border);">
|
|
17029
|
+
<button id="codeModalTabCode" class="btn btn-secondary" style="padding: 6px 10px; min-width: auto;" onclick="switchCodeModalTab('code')">Script</button>
|
|
17030
|
+
${hasVarsTab ? `
|
|
17031
|
+
<button id="codeModalTabVars" class="btn btn-secondary" style="padding: 6px 10px; min-width: auto; opacity: .7;" onclick="switchCodeModalTab('vars')">
|
|
17032
|
+
Test Variables
|
|
17033
|
+
<span id="codeModalVarsTabBadge" style="margin-left:6px; font-size: 10px; color: var(--text-muted);">...</span>
|
|
17034
|
+
</button>
|
|
17035
|
+
` : ''}
|
|
17036
|
+
</div>
|
|
17037
|
+
${resolvedType === 'mobile' ? `
|
|
17038
|
+
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; padding: 10px 16px; border-bottom: 1px solid var(--border); flex-wrap: wrap; background: rgba(255,255,255,0.01);">
|
|
17039
|
+
<div style="display:flex; align-items:center; gap:8px; flex-wrap: wrap; min-width: 0;">
|
|
17040
|
+
<label for="codeModalMobileDeviceSelect" style="font-size: 12px; color: var(--text-secondary);">Run on device</label>
|
|
17041
|
+
<select id="codeModalMobileDeviceSelect" class="setting-input" style="min-width: 280px; max-width: 100%; padding: 6px 10px; height: auto;" onchange="onCodeModalMobileDeviceChanged(this.value)">
|
|
17042
|
+
<option value="">Auto-detect device (Maestro default)</option>
|
|
17043
|
+
</select>
|
|
17044
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="loadCodeModalMobileRunDevices(true)">Refresh</button>
|
|
17045
|
+
</div>
|
|
17046
|
+
<div id="codeModalMobileDeviceStatus" style="font-size: 11px; color: var(--text-muted);">Loading devices...</div>
|
|
17047
|
+
</div>
|
|
17048
|
+
` : ''}
|
|
16318
17049
|
<div class="code-modal-body">
|
|
16319
|
-
<div class="code-
|
|
16320
|
-
<div
|
|
16321
|
-
|
|
17050
|
+
<div id="codeModalCodePanel" class="code-modal-panel">
|
|
17051
|
+
<div class="code-editor-container">
|
|
17052
|
+
<div id="codeLineNumbers" class="code-line-numbers">${lineNumbers}</div>
|
|
17053
|
+
<textarea id="codeEditorTextarea" class="code-editor-textarea" spellcheck="false">${escapeHtml(code)}</textarea>
|
|
17054
|
+
</div>
|
|
17055
|
+
<div id="codeModalOutput" class="code-modal-output" style="display: none;"></div>
|
|
16322
17056
|
</div>
|
|
16323
|
-
|
|
17057
|
+
${hasVarsTab ? `
|
|
17058
|
+
<div id="codeModalVarsPanel" class="code-modal-panel" style="display:none; padding: 12px 0 0 0; overflow:auto;">
|
|
17059
|
+
<div style="display:grid; gap:10px;">
|
|
17060
|
+
<div style="display:flex; justify-content: space-between; gap: 12px; align-items: center; flex-wrap: wrap;">
|
|
17061
|
+
<div id="codeModalVarSummary" style="font-size: 12px; color: var(--text-secondary); display:flex; gap:10px; flex-wrap:wrap;">
|
|
17062
|
+
<span>Loading variables...</span>
|
|
17063
|
+
</div>
|
|
17064
|
+
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
17065
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="addMissingCodeModalVariablesFromScript()">Add Missing From Script</button>
|
|
17066
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="addCodeModalVariableRow()">Add Variable</button>
|
|
17067
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="loadCodeModalVariables()">Reload</button>
|
|
17068
|
+
<button class="btn btn-primary" style="padding:6px 10px; min-width:auto;" onclick="saveCodeModalVariables()">Save Variables</button>
|
|
17069
|
+
</div>
|
|
17070
|
+
</div>
|
|
17071
|
+
<div id="codeModalMissingList" style="font-size:11px; color: var(--text-muted);">Checking placeholders...</div>
|
|
17072
|
+
<div class="code-vars-table-wrap">
|
|
17073
|
+
<table style="width:100%; border-collapse: collapse; min-width: 860px; background: var(--bg-secondary);">
|
|
17074
|
+
<thead>
|
|
17075
|
+
<tr style="text-align:left; font-size: 11px; color: var(--text-secondary); background: var(--bg-tertiary);">
|
|
17076
|
+
<th style="padding:8px;">Key</th>
|
|
17077
|
+
<th style="padding:8px;">Value</th>
|
|
17078
|
+
<th style="padding:8px;">Platform</th>
|
|
17079
|
+
<th style="padding:8px;">Secret</th>
|
|
17080
|
+
<th style="padding:8px;">Notes</th>
|
|
17081
|
+
<th style="padding:8px;"></th>
|
|
17082
|
+
</tr>
|
|
17083
|
+
</thead>
|
|
17084
|
+
<tbody id="codeModalVarsTableBody"></tbody>
|
|
17085
|
+
</table>
|
|
17086
|
+
</div>
|
|
17087
|
+
<div style="display:grid; gap:6px;">
|
|
17088
|
+
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
|
17089
|
+
<label style="font-size: 12px; color: var(--text-secondary);">.env.test (saved in DB as structured variables)</label>
|
|
17090
|
+
<div style="display:flex; gap:8px;">
|
|
17091
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="syncCodeModalEnvFromTable()">Regenerate</button>
|
|
17092
|
+
<button class="btn btn-secondary" style="padding:6px 10px; min-width:auto;" onclick="applyEnvTestToCodeModalTable()">Apply .env</button>
|
|
17093
|
+
</div>
|
|
17094
|
+
</div>
|
|
17095
|
+
<textarea id="codeModalEnvTextarea" class="code-editor-textarea" spellcheck="false" style="min-height: 140px; height: 140px;"></textarea>
|
|
17096
|
+
</div>
|
|
17097
|
+
</div>
|
|
17098
|
+
</div>
|
|
17099
|
+
` : ''}
|
|
16324
17100
|
</div>
|
|
16325
17101
|
<div class="code-modal-footer">
|
|
16326
|
-
${
|
|
16327
|
-
<button id="runCodeBtn" class="btn btn-run" onclick="runMaestroFromEditor('${
|
|
17102
|
+
${resolvedRecordingId ? `
|
|
17103
|
+
<button id="runCodeBtn" class="btn btn-run" onclick="${isYaml ? `runMaestroFromEditor('${resolvedRecordingId}')` : `runPlaywrightFromEditor('${resolvedRecordingId}')`}">
|
|
16328
17104
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
16329
17105
|
<polygon points="5 3 19 12 5 21 5 3"/>
|
|
16330
17106
|
</svg>
|
|
16331
|
-
Run Test
|
|
17107
|
+
${isYaml ? 'Run on Device' : 'Run Test'}
|
|
16332
17108
|
</button>
|
|
17109
|
+
${isYaml ? `
|
|
17110
|
+
<button id="stopCodeBtn" class="btn btn-secondary" onclick="stopMaestroRunFromEditor()" style="display:none; min-width: 120px; border-color: rgba(239,68,68,0.25); color: #fca5a5;">
|
|
17111
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
|
|
17112
|
+
<rect x="6" y="6" width="12" height="12" rx="1"/>
|
|
17113
|
+
</svg>
|
|
17114
|
+
Stop Run
|
|
17115
|
+
</button>
|
|
17116
|
+
` : ''}
|
|
16333
17117
|
` : ''}
|
|
16334
17118
|
<button class="btn btn-secondary" onclick="copyCodeToClipboard()">
|
|
16335
17119
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
@@ -16338,8 +17122,8 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16338
17122
|
</svg>
|
|
16339
17123
|
Copy
|
|
16340
17124
|
</button>
|
|
16341
|
-
${
|
|
16342
|
-
<button class="btn btn-primary" onclick="saveCodeChanges('${
|
|
17125
|
+
${resolvedRecordingId ? `
|
|
17126
|
+
<button class="btn btn-primary" onclick="saveCodeChanges('${resolvedRecordingId}', '${resolvedType}')">
|
|
16343
17127
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
16344
17128
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
|
16345
17129
|
<polyline points="17 21 17 13 7 13 7 21"/>
|
|
@@ -16352,6 +17136,8 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16352
17136
|
</div>
|
|
16353
17137
|
`;
|
|
16354
17138
|
document.body.appendChild(modal);
|
|
17139
|
+
lockPageScrollForCodeModal();
|
|
17140
|
+
attachCodeModalScrollTrap(modal);
|
|
16355
17141
|
requestAnimationFrame(() => modal.classList.add('active'));
|
|
16356
17142
|
|
|
16357
17143
|
// Sync line numbers with textarea scroll and content
|
|
@@ -16364,9 +17150,17 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16364
17150
|
lineNumbersEl.scrollTop = textarea.scrollTop;
|
|
16365
17151
|
});
|
|
16366
17152
|
|
|
17153
|
+
// If the pointer is over line numbers, scroll the editor instead of the page behind
|
|
17154
|
+
lineNumbersEl.addEventListener('wheel', (e) => {
|
|
17155
|
+
e.preventDefault();
|
|
17156
|
+
textarea.scrollTop += e.deltaY;
|
|
17157
|
+
textarea.scrollLeft += e.deltaX;
|
|
17158
|
+
}, { passive: false });
|
|
17159
|
+
|
|
16367
17160
|
// Update line numbers when content changes
|
|
16368
17161
|
textarea.addEventListener('input', () => {
|
|
16369
17162
|
updateLineNumbers();
|
|
17163
|
+
updateCodeModalVariablesStatus();
|
|
16370
17164
|
});
|
|
16371
17165
|
}
|
|
16372
17166
|
|
|
@@ -16380,6 +17174,14 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16380
17174
|
};
|
|
16381
17175
|
document.addEventListener('keydown', escHandler);
|
|
16382
17176
|
modal._escHandler = escHandler;
|
|
17177
|
+
|
|
17178
|
+
if (hasVarsTab) {
|
|
17179
|
+
loadCodeModalVariables();
|
|
17180
|
+
}
|
|
17181
|
+
if (resolvedType === 'mobile') {
|
|
17182
|
+
loadCodeModalMobileRunDevices();
|
|
17183
|
+
}
|
|
17184
|
+
switchCodeModalTab('code');
|
|
16383
17185
|
}
|
|
16384
17186
|
|
|
16385
17187
|
function updateLineNumbers() {
|
|
@@ -16392,17 +17194,279 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16392
17194
|
lineNumbersEl.textContent = Array.from({length: maxLines}, (_, i) => i + 1).join('\n');
|
|
16393
17195
|
}
|
|
16394
17196
|
|
|
17197
|
+
function setCodeModalRunButtonBusy(runBtn, label) {
|
|
17198
|
+
if (!runBtn) return;
|
|
17199
|
+
runBtn.disabled = true;
|
|
17200
|
+
runBtn.innerHTML = `
|
|
17201
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px; animation: spin 1s linear infinite;">
|
|
17202
|
+
<circle cx="12" cy="12" r="10"/>
|
|
17203
|
+
<path d="M12 6v6l4 2"/>
|
|
17204
|
+
</svg>
|
|
17205
|
+
${escapeHtml(label || 'Running...')}
|
|
17206
|
+
`;
|
|
17207
|
+
}
|
|
17208
|
+
|
|
17209
|
+
function setCodeModalStopButtonState(stopBtn, options = {}) {
|
|
17210
|
+
if (!stopBtn) return;
|
|
17211
|
+
const {
|
|
17212
|
+
visible = false,
|
|
17213
|
+
disabled = false,
|
|
17214
|
+
label = 'Stop Run'
|
|
17215
|
+
} = options;
|
|
17216
|
+
stopBtn.style.display = visible ? '' : 'none';
|
|
17217
|
+
stopBtn.disabled = !!disabled;
|
|
17218
|
+
stopBtn.innerHTML = `
|
|
17219
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 4px;">
|
|
17220
|
+
<rect x="6" y="6" width="12" height="12" rx="1"/>
|
|
17221
|
+
</svg>
|
|
17222
|
+
${escapeHtml(label)}
|
|
17223
|
+
`;
|
|
17224
|
+
}
|
|
17225
|
+
|
|
17226
|
+
function resetCodeModalRunButton(runBtn, type = 'mobile') {
|
|
17227
|
+
if (!runBtn) return;
|
|
17228
|
+
runBtn.disabled = false;
|
|
17229
|
+
runBtn.innerHTML = `
|
|
17230
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
|
|
17231
|
+
<polygon points="5 3 19 12 5 21 5 3"/>
|
|
17232
|
+
</svg>
|
|
17233
|
+
${type === 'mobile' ? 'Run on Device' : 'Run Test'}
|
|
17234
|
+
`;
|
|
17235
|
+
}
|
|
17236
|
+
|
|
17237
|
+
function appendCodeModalOutputLine(outputEl, htmlLine) {
|
|
17238
|
+
if (!outputEl) return;
|
|
17239
|
+
outputEl.innerHTML += '\n' + htmlLine;
|
|
17240
|
+
outputEl.scrollTop = outputEl.scrollHeight;
|
|
17241
|
+
}
|
|
17242
|
+
|
|
17243
|
+
async function pollMaestroRunStatus(runId, options = {}) {
|
|
17244
|
+
const {
|
|
17245
|
+
outputEl = null,
|
|
17246
|
+
runBtn = null,
|
|
17247
|
+
pollMs = 1200,
|
|
17248
|
+
timeoutMs = 360000
|
|
17249
|
+
} = options;
|
|
17250
|
+
const startedAt = Date.now();
|
|
17251
|
+
let runningAnnounced = false;
|
|
17252
|
+
|
|
17253
|
+
while ((Date.now() - startedAt) < timeoutMs) {
|
|
17254
|
+
const response = await fetch(`/api/testing/mobile/replays/${encodeURIComponent(runId)}`);
|
|
17255
|
+
const data = await response.json().catch(() => ({}));
|
|
17256
|
+
if (!response.ok) {
|
|
17257
|
+
throw new Error(data.error || 'Failed to read Maestro run status');
|
|
17258
|
+
}
|
|
17259
|
+
|
|
17260
|
+
const elapsedSec = Math.max(0, Math.round((Number(data.elapsedMs || 0) / 1000)));
|
|
17261
|
+
if (data.status === 'running') {
|
|
17262
|
+
if (!runningAnnounced && outputEl) {
|
|
17263
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-running">⏳ Maestro is running on device...</span>');
|
|
17264
|
+
runningAnnounced = true;
|
|
17265
|
+
}
|
|
17266
|
+
if (runBtn) {
|
|
17267
|
+
setCodeModalRunButtonBusy(runBtn, elapsedSec > 0 ? `Running... ${elapsedSec}s` : 'Running...');
|
|
17268
|
+
}
|
|
17269
|
+
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
17270
|
+
continue;
|
|
17271
|
+
}
|
|
17272
|
+
|
|
17273
|
+
return data;
|
|
17274
|
+
}
|
|
17275
|
+
|
|
17276
|
+
throw new Error('Timed out waiting for Maestro run to finish');
|
|
17277
|
+
}
|
|
17278
|
+
|
|
16395
17279
|
// Run Maestro test from editor
|
|
16396
17280
|
let codeModalRunning = false;
|
|
17281
|
+
async function stopMaestroRunFromEditor() {
|
|
17282
|
+
const stopBtn = document.getElementById('stopCodeBtn');
|
|
17283
|
+
const outputEl = document.getElementById('codeModalOutput');
|
|
17284
|
+
const runId = String(codeModalState?.activeMaestroRunId || '').trim();
|
|
17285
|
+
if (!runId) return;
|
|
17286
|
+
|
|
17287
|
+
try {
|
|
17288
|
+
setCodeModalStopButtonState(stopBtn, { visible: true, disabled: true, label: 'Stopping...' });
|
|
17289
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-running">⏹ Requesting stop...</span>');
|
|
17290
|
+
const response = await fetch(`/api/testing/mobile/replays/${encodeURIComponent(runId)}/stop`, {
|
|
17291
|
+
method: 'POST'
|
|
17292
|
+
});
|
|
17293
|
+
const data = await response.json().catch(() => ({}));
|
|
17294
|
+
if (!response.ok && response.status !== 409) {
|
|
17295
|
+
throw new Error(data.error || 'Failed to stop run');
|
|
17296
|
+
}
|
|
17297
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">⏳ Stop requested. Waiting for runner status...</span>');
|
|
17298
|
+
showToast('Stop requested', 'info');
|
|
17299
|
+
} catch (error) {
|
|
17300
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-error">✗ Stop failed: ' + escapeHtml(error.message || 'Unknown error') + '</span>');
|
|
17301
|
+
setCodeModalStopButtonState(stopBtn, { visible: true, disabled: false, label: 'Stop Run' });
|
|
17302
|
+
showToast('Failed to stop run', 'error');
|
|
17303
|
+
}
|
|
17304
|
+
}
|
|
17305
|
+
|
|
16397
17306
|
async function runMaestroFromEditor(recordingId) {
|
|
16398
17307
|
if (codeModalRunning) return;
|
|
16399
17308
|
|
|
16400
17309
|
const textarea = document.getElementById('codeEditorTextarea');
|
|
16401
17310
|
const outputEl = document.getElementById('codeModalOutput');
|
|
16402
17311
|
const runBtn = document.getElementById('runCodeBtn');
|
|
17312
|
+
const stopBtn = document.getElementById('stopCodeBtn');
|
|
16403
17313
|
|
|
16404
17314
|
if (!textarea || !outputEl) return;
|
|
16405
17315
|
|
|
17316
|
+
codeModalRunning = true;
|
|
17317
|
+
if (codeModalState) codeModalState.activeMaestroRunId = null;
|
|
17318
|
+
if (runBtn) {
|
|
17319
|
+
setCodeModalRunButtonBusy(runBtn, 'Loading...');
|
|
17320
|
+
}
|
|
17321
|
+
setCodeModalStopButtonState(stopBtn, { visible: false });
|
|
17322
|
+
|
|
17323
|
+
// Show output area
|
|
17324
|
+
outputEl.style.display = 'block';
|
|
17325
|
+
outputEl.innerHTML = '<span class="output-running">▶ Saving and running Maestro test on device...</span>';
|
|
17326
|
+
|
|
17327
|
+
const selectedDeviceId = String(codeModalState?.mobileRunSelectedDeviceId || '').trim();
|
|
17328
|
+
const selectedDevice = Array.isArray(codeModalState?.mobileRunDevices)
|
|
17329
|
+
? codeModalState.mobileRunDevices.find((d) => String(d?.id || '').trim() === selectedDeviceId)
|
|
17330
|
+
: null;
|
|
17331
|
+
|
|
17332
|
+
try {
|
|
17333
|
+
const currentVars = collectCodeModalVariablesFromTable().map((row) => ({
|
|
17334
|
+
...row,
|
|
17335
|
+
key: String(row.key || '').trim().toUpperCase(),
|
|
17336
|
+
platform: ['mobile', 'web', 'both'].includes(row.platform) ? row.platform : 'both'
|
|
17337
|
+
})).filter((row) => row.key);
|
|
17338
|
+
const currentVarsSignature = getCodeModalVariablesSignature(currentVars);
|
|
17339
|
+
const shouldSaveVars = !codeModalState?.variablesLoaded
|
|
17340
|
+
|| currentVarsSignature !== String(codeModalState?.lastSavedVariablesSignature || '');
|
|
17341
|
+
if (shouldSaveVars) {
|
|
17342
|
+
const varsSaved = await saveCodeModalVariables({ silent: true });
|
|
17343
|
+
if (!varsSaved) throw new Error('Failed to save test variables');
|
|
17344
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-success">✓ Test variables saved</span>');
|
|
17345
|
+
} else {
|
|
17346
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">↺ Test variables unchanged (skipped save)</span>');
|
|
17347
|
+
}
|
|
17348
|
+
|
|
17349
|
+
// First, save the code
|
|
17350
|
+
const currentCode = textarea.value;
|
|
17351
|
+
const shouldSaveCode = currentCode !== String(codeModalState?.lastSavedCode ?? codeModalState?.code ?? '');
|
|
17352
|
+
if (shouldSaveCode) {
|
|
17353
|
+
const saveResponse = await fetch(`/api/testing/mobile/recordings/${recordingId}/flow`, {
|
|
17354
|
+
method: 'PUT',
|
|
17355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17356
|
+
body: JSON.stringify({ flowCode: currentCode })
|
|
17357
|
+
});
|
|
17358
|
+
|
|
17359
|
+
if (!saveResponse.ok) {
|
|
17360
|
+
throw new Error('Failed to save flow code');
|
|
17361
|
+
}
|
|
17362
|
+
if (codeModalState) {
|
|
17363
|
+
codeModalState.code = currentCode;
|
|
17364
|
+
codeModalState.lastSavedCode = currentCode;
|
|
17365
|
+
}
|
|
17366
|
+
outputEl.innerHTML += '\n<span class="output-success">✓ Flow saved</span>';
|
|
17367
|
+
} else {
|
|
17368
|
+
outputEl.innerHTML += '\n<span class="output-step">↺ Flow unchanged (skipped save)</span>';
|
|
17369
|
+
}
|
|
17370
|
+
outputEl.innerHTML += '\n<span class="output-running">▶ Starting Maestro test on device...</span>';
|
|
17371
|
+
|
|
17372
|
+
// Now run the test
|
|
17373
|
+
const runRequestBody = {};
|
|
17374
|
+
if (selectedDeviceId) {
|
|
17375
|
+
runRequestBody.deviceId = selectedDeviceId;
|
|
17376
|
+
if (selectedDevice?.platform === 'ios' || selectedDevice?.platform === 'android') {
|
|
17377
|
+
runRequestBody.devicePlatform = selectedDevice.platform;
|
|
17378
|
+
runRequestBody.deviceName = String(selectedDevice?.name || selectedDeviceId);
|
|
17379
|
+
runRequestBody.skipDeviceValidation = true;
|
|
17380
|
+
}
|
|
17381
|
+
}
|
|
17382
|
+
const runResponse = await fetch(`/api/testing/mobile/recordings/${recordingId}/replay`, {
|
|
17383
|
+
method: 'POST',
|
|
17384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17385
|
+
body: JSON.stringify(runRequestBody)
|
|
17386
|
+
});
|
|
17387
|
+
|
|
17388
|
+
const runData = await runResponse.json();
|
|
17389
|
+
|
|
17390
|
+
if (!runResponse.ok) {
|
|
17391
|
+
if (Array.isArray(runData?.missingKeys) && runData.missingKeys.length > 0) {
|
|
17392
|
+
outputEl.innerHTML += '\n<span class="output-error">✗ Missing variables: ' + runData.missingKeys.join(', ') + '</span>';
|
|
17393
|
+
switchCodeModalTab('vars');
|
|
17394
|
+
}
|
|
17395
|
+
throw new Error(runData.error || 'Failed to start test');
|
|
17396
|
+
}
|
|
17397
|
+
|
|
17398
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-success">✓ Maestro test started!</span>');
|
|
17399
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">📍 Flow: ' + escapeHtml(runData.flowPath || recordingId) + '</span>');
|
|
17400
|
+
if (runData?.deviceName || runData?.deviceId) {
|
|
17401
|
+
const deviceLabel = runData.deviceName || runData.deviceId;
|
|
17402
|
+
const platformLabel = runData.devicePlatform ? ` (${String(runData.devicePlatform).toUpperCase()})` : '';
|
|
17403
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">📱 Device: ' + escapeHtml(deviceLabel + platformLabel) + '</span>');
|
|
17404
|
+
if (runData?.deviceSelectionSource === 'trusted-client') {
|
|
17405
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">⚡ Fast start: reused selected device from modal</span>');
|
|
17406
|
+
}
|
|
17407
|
+
} else {
|
|
17408
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">📱 Device: Auto-detect (Maestro default)</span>');
|
|
17409
|
+
}
|
|
17410
|
+
if (Array.isArray(runData.usedKeys) && runData.usedKeys.length > 0) {
|
|
17411
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">🔐 Vars: ' + escapeHtml(runData.usedKeys.join(', ')) + '</span>');
|
|
17412
|
+
}
|
|
17413
|
+
appendCodeModalOutputLine(outputEl, '<span style="color: #fbbf24;">💡 Watch the selected device for test execution.</span>');
|
|
17414
|
+
|
|
17415
|
+
const runId = typeof runData?.runId === 'string' ? runData.runId : '';
|
|
17416
|
+
if (runId) {
|
|
17417
|
+
if (codeModalState) codeModalState.activeMaestroRunId = runId;
|
|
17418
|
+
setCodeModalRunButtonBusy(runBtn, 'Running...');
|
|
17419
|
+
setCodeModalStopButtonState(stopBtn, { visible: true, disabled: false, label: 'Stop Run' });
|
|
17420
|
+
const finalStatus = await pollMaestroRunStatus(runId, { outputEl, runBtn });
|
|
17421
|
+
const durationMs = Number(finalStatus?.durationMs);
|
|
17422
|
+
const durationLabel = Number.isFinite(durationMs) && durationMs > 0
|
|
17423
|
+
? ` (${Math.round(durationMs / 1000)}s)`
|
|
17424
|
+
: '';
|
|
17425
|
+
|
|
17426
|
+
if (finalStatus.status === 'completed') {
|
|
17427
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-success">✓ Maestro test completed' + escapeHtml(durationLabel) + '</span>');
|
|
17428
|
+
if (finalStatus.output) {
|
|
17429
|
+
const preview = String(finalStatus.output).slice(-2500);
|
|
17430
|
+
appendCodeModalOutputLine(outputEl, '<pre style="white-space: pre-wrap; margin: 8px 0 0 0; color: var(--text-secondary);">' + escapeHtml(preview) + '</pre>');
|
|
17431
|
+
}
|
|
17432
|
+
showToast('Maestro test completed', 'success');
|
|
17433
|
+
} else if (finalStatus.status === 'failed') {
|
|
17434
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-error">✗ Maestro test failed' + escapeHtml(durationLabel) + '</span>');
|
|
17435
|
+
if (finalStatus.error) {
|
|
17436
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-error">' + escapeHtml(String(finalStatus.error)) + '</span>');
|
|
17437
|
+
}
|
|
17438
|
+
if (finalStatus.output) {
|
|
17439
|
+
const preview = String(finalStatus.output).slice(-2500);
|
|
17440
|
+
appendCodeModalOutputLine(outputEl, '<pre style="white-space: pre-wrap; margin: 8px 0 0 0; color: var(--text-secondary);">' + escapeHtml(preview) + '</pre>');
|
|
17441
|
+
}
|
|
17442
|
+
showToast('Maestro test failed', 'error');
|
|
17443
|
+
} else if (finalStatus.status === 'canceled') {
|
|
17444
|
+
appendCodeModalOutputLine(outputEl, '<span class="output-step">⏹ Maestro run canceled' + escapeHtml(durationLabel) + '</span>');
|
|
17445
|
+
showToast('Maestro run canceled', 'info');
|
|
17446
|
+
}
|
|
17447
|
+
} else {
|
|
17448
|
+
showToast('Maestro test started on device', 'success');
|
|
17449
|
+
}
|
|
17450
|
+
|
|
17451
|
+
} catch (error) {
|
|
17452
|
+
outputEl.innerHTML += '\n<span class="output-error">✗ Error: ' + (error.message || 'Unknown error') + '</span>';
|
|
17453
|
+
showToast('Failed to run test', 'error');
|
|
17454
|
+
} finally {
|
|
17455
|
+
codeModalRunning = false;
|
|
17456
|
+
if (codeModalState) codeModalState.activeMaestroRunId = null;
|
|
17457
|
+
setCodeModalStopButtonState(stopBtn, { visible: false });
|
|
17458
|
+
resetCodeModalRunButton(runBtn, 'mobile');
|
|
17459
|
+
}
|
|
17460
|
+
}
|
|
17461
|
+
|
|
17462
|
+
async function runPlaywrightFromEditor(recordingId) {
|
|
17463
|
+
if (codeModalRunning) return;
|
|
17464
|
+
|
|
17465
|
+
const textarea = document.getElementById('codeEditorTextarea');
|
|
17466
|
+
const outputEl = document.getElementById('codeModalOutput');
|
|
17467
|
+
const runBtn = document.getElementById('runCodeBtn');
|
|
17468
|
+
if (!textarea || !outputEl) return;
|
|
17469
|
+
|
|
16406
17470
|
codeModalRunning = true;
|
|
16407
17471
|
if (runBtn) {
|
|
16408
17472
|
runBtn.disabled = true;
|
|
@@ -16415,84 +17479,66 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16415
17479
|
`;
|
|
16416
17480
|
}
|
|
16417
17481
|
|
|
16418
|
-
// Show output area
|
|
16419
17482
|
outputEl.style.display = 'block';
|
|
16420
|
-
outputEl.innerHTML = '<span class="output-running">▶ Saving and running
|
|
16421
|
-
|
|
16422
|
-
// Detect platform from YAML appId
|
|
16423
|
-
const yamlContent = textarea.value;
|
|
16424
|
-
const appIdMatch = yamlContent.match(/appId:\s*([^\s\n]+)/);
|
|
16425
|
-
const appId = appIdMatch ? appIdMatch[1] : '';
|
|
16426
|
-
const platform = appId.includes('com.apple') || appId.startsWith('com.') && !appId.includes('android') ? 'ios' : 'android';
|
|
17483
|
+
outputEl.innerHTML = '<span class="output-running">▶ Saving Playwright script and running test...</span>';
|
|
16427
17484
|
|
|
16428
17485
|
try {
|
|
16429
|
-
|
|
16430
|
-
|
|
17486
|
+
const varsSaved = await saveCodeModalVariables({ silent: true });
|
|
17487
|
+
if (varsSaved === false) {
|
|
17488
|
+
throw new Error('Failed to save test variables');
|
|
17489
|
+
}
|
|
17490
|
+
|
|
17491
|
+
const saveResponse = await fetch(`/api/recorder/recordings/${recordingId}/spec`, {
|
|
16431
17492
|
method: 'PUT',
|
|
16432
17493
|
headers: { 'Content-Type': 'application/json' },
|
|
16433
|
-
body: JSON.stringify({
|
|
17494
|
+
body: JSON.stringify({ specCode: textarea.value })
|
|
16434
17495
|
});
|
|
16435
|
-
|
|
17496
|
+
const saveData = await saveResponse.json().catch(() => ({}));
|
|
16436
17497
|
if (!saveResponse.ok) {
|
|
16437
|
-
throw new Error('Failed to save
|
|
17498
|
+
throw new Error(saveData.error || 'Failed to save spec');
|
|
16438
17499
|
}
|
|
16439
17500
|
|
|
16440
|
-
outputEl.innerHTML += '\n<span class="output-success">✓
|
|
16441
|
-
outputEl.innerHTML += '\n<span class="output-running">▶ Starting auto-capture and Maestro test...</span>';
|
|
17501
|
+
outputEl.innerHTML += '\n<span class="output-success">✓ Spec saved</span>';
|
|
16442
17502
|
|
|
16443
|
-
|
|
16444
|
-
try {
|
|
16445
|
-
await fetch('/api/testing/mobile/auto-capture/start', {
|
|
16446
|
-
method: 'POST',
|
|
16447
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16448
|
-
body: JSON.stringify({ recordingId, platform })
|
|
16449
|
-
});
|
|
16450
|
-
outputEl.innerHTML += '\n<span class="output-success">✓ Auto-capture started</span>';
|
|
16451
|
-
} catch (captureErr) {
|
|
16452
|
-
console.log('Auto-capture not available:', captureErr);
|
|
16453
|
-
}
|
|
16454
|
-
|
|
16455
|
-
// Now run the test
|
|
16456
|
-
const runResponse = await fetch(`/api/testing/mobile/recordings/${recordingId}/replay`, {
|
|
17503
|
+
const runResponse = await fetch(`/api/recorder/recordings/${recordingId}/run`, {
|
|
16457
17504
|
method: 'POST',
|
|
16458
|
-
headers: { 'Content-Type': 'application/json' }
|
|
17505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17506
|
+
body: JSON.stringify({
|
|
17507
|
+
headless: false,
|
|
17508
|
+
browser: 'chromium',
|
|
17509
|
+
video: 'retain-on-failure',
|
|
17510
|
+
screenshot: 'only-on-failure',
|
|
17511
|
+
trace: 'retain-on-failure'
|
|
17512
|
+
})
|
|
16459
17513
|
});
|
|
16460
|
-
|
|
16461
17514
|
const runData = await runResponse.json();
|
|
16462
17515
|
|
|
16463
17516
|
if (!runResponse.ok) {
|
|
16464
|
-
|
|
17517
|
+
if (Array.isArray(runData?.missingKeys) && runData.missingKeys.length > 0) {
|
|
17518
|
+
outputEl.innerHTML += '\n<span class="output-error">✗ Missing variables: ' + runData.missingKeys.join(', ') + '</span>';
|
|
17519
|
+
switchCodeModalTab('vars');
|
|
17520
|
+
}
|
|
17521
|
+
throw new Error(runData.error || runData.result?.error || 'Failed to run Playwright test');
|
|
16465
17522
|
}
|
|
16466
17523
|
|
|
16467
|
-
|
|
16468
|
-
outputEl.innerHTML += '\n<span class="output-
|
|
16469
|
-
outputEl.innerHTML += '\n
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
body: JSON.stringify({ recordingId })
|
|
16481
|
-
});
|
|
16482
|
-
} catch {}
|
|
16483
|
-
}, 30000);
|
|
17524
|
+
const result = runData.result || {};
|
|
17525
|
+
outputEl.innerHTML += '\n<span class="output-success">✓ Playwright run completed</span>';
|
|
17526
|
+
outputEl.innerHTML += '\n<span class="output-step">✅ Passed: ' + (result.passed ?? 0) + ' | ❌ Failed: ' + (result.failed ?? 0) + ' | ⏭️ Skipped: ' + (result.skipped ?? 0) + '</span>';
|
|
17527
|
+
if (Array.isArray(runData.usedKeys) && runData.usedKeys.length > 0) {
|
|
17528
|
+
outputEl.innerHTML += '\n<span class="output-step">🔐 Vars: ' + runData.usedKeys.join(', ') + '</span>';
|
|
17529
|
+
}
|
|
17530
|
+
if (result.reportPath) {
|
|
17531
|
+
outputEl.innerHTML += '\n<span class="output-step">📄 Report: ' + result.reportPath + '</span>';
|
|
17532
|
+
}
|
|
17533
|
+
if (result.output) {
|
|
17534
|
+
const preview = String(result.output).slice(0, 2500);
|
|
17535
|
+
outputEl.innerHTML += '\n\n<pre style="white-space: pre-wrap; margin: 8px 0 0 0; color: var(--text-secondary);">' + escapeHtml(preview) + '</pre>';
|
|
17536
|
+
}
|
|
16484
17537
|
|
|
17538
|
+
showToast(result.success === false ? 'Playwright test failed' : 'Playwright test finished', result.success === false ? 'warning' : 'success');
|
|
16485
17539
|
} catch (error) {
|
|
16486
17540
|
outputEl.innerHTML += '\n<span class="output-error">✗ Error: ' + (error.message || 'Unknown error') + '</span>';
|
|
16487
|
-
showToast('Failed to run test', 'error');
|
|
16488
|
-
// Stop auto-capture on error
|
|
16489
|
-
try {
|
|
16490
|
-
await fetch('/api/testing/mobile/auto-capture/stop', {
|
|
16491
|
-
method: 'POST',
|
|
16492
|
-
headers: { 'Content-Type': 'application/json' },
|
|
16493
|
-
body: JSON.stringify({ recordingId })
|
|
16494
|
-
});
|
|
16495
|
-
} catch {}
|
|
17541
|
+
showToast('Failed to run Playwright test', 'error');
|
|
16496
17542
|
} finally {
|
|
16497
17543
|
codeModalRunning = false;
|
|
16498
17544
|
if (runBtn) {
|
|
@@ -16513,8 +17559,13 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16513
17559
|
if (modal._escHandler) {
|
|
16514
17560
|
document.removeEventListener('keydown', modal._escHandler);
|
|
16515
17561
|
}
|
|
17562
|
+
if (modal._wheelTrapHandler) {
|
|
17563
|
+
modal.removeEventListener('wheel', modal._wheelTrapHandler, { capture: true });
|
|
17564
|
+
}
|
|
16516
17565
|
modal.remove();
|
|
16517
17566
|
}
|
|
17567
|
+
unlockPageScrollForCodeModal();
|
|
17568
|
+
codeModalState = null;
|
|
16518
17569
|
}
|
|
16519
17570
|
|
|
16520
17571
|
function copyCodeToClipboard() {
|
|
@@ -16532,6 +17583,11 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16532
17583
|
const code = textarea.value;
|
|
16533
17584
|
|
|
16534
17585
|
try {
|
|
17586
|
+
const varsSaved = await saveCodeModalVariables({ silent: true });
|
|
17587
|
+
if (varsSaved === false) {
|
|
17588
|
+
throw new Error('Failed to save test variables');
|
|
17589
|
+
}
|
|
17590
|
+
|
|
16535
17591
|
if (type === 'mobile') {
|
|
16536
17592
|
const response = await fetch(`/api/testing/mobile/recordings/${recordingId}/flow`, {
|
|
16537
17593
|
method: 'PUT',
|
|
@@ -16549,6 +17605,12 @@ appId: ${platform === 'ios' ? 'com.apple.Preferences' : 'com.android.settings'}
|
|
|
16549
17605
|
if (!response.ok) throw new Error('Failed to save');
|
|
16550
17606
|
}
|
|
16551
17607
|
|
|
17608
|
+
if (codeModalState) {
|
|
17609
|
+
codeModalState.code = code;
|
|
17610
|
+
codeModalState.lastSavedCode = code;
|
|
17611
|
+
codeModalState.placeholders = extractCodeModalPlaceholders(code);
|
|
17612
|
+
}
|
|
17613
|
+
updateCodeModalVariablesStatus();
|
|
16552
17614
|
showToast('Code saved!', 'success');
|
|
16553
17615
|
} catch (error) {
|
|
16554
17616
|
showToast('Failed to save code', 'error');
|