@@ -473,71 +473,149 @@ function invalidateMonacoCache() {
473473 _monacoLastCheck = 0 ;
474474}
475475
476- // Open the Pine Editor using keyboard shortcut first (fastest) , then DOM fallback
476+ // Open the Pine Editor — tries TV API first , then DOM buttons, then keyboard shortcut
477477async function openPineEditor ( ) {
478- // Primary: Alt+P is the TradingView keyboard shortcut for Pine Editor on most platforms
479- // We also try the bottom bar API and known DOM selectors
480- await evaluate ( `
478+ // Step 1: TV internal API (most reliable when available)
479+ const apiOk = await evaluate ( `
481480 (function() {
482- // TV API
483481 try {
484482 var bwb = window.TradingView && window.TradingView.bottomWidgetBar;
485483 if (bwb) {
486- if (typeof bwb.activateScriptEditorTab === 'function') { bwb.activateScriptEditorTab(); return; }
487- if (typeof bwb.showWidget === 'function') { bwb.showWidget('pine-editor'); return; }
484+ if (typeof bwb.activateScriptEditorTab === 'function') { bwb.activateScriptEditorTab(); return true ; }
485+ if (typeof bwb.showWidget === 'function') { bwb.showWidget('pine-editor'); return true ; }
488486 }
489487 } catch(e) {}
490-
491- // DOM buttons — sorted by most likely to exist
492- var sels = [
493- '[data-name="pine-editor-toolbar"]',
494- '[data-name="pine-dialog-button"]',
495- '[aria-label="Pine Editor"]',
496- '[aria-label="Pine"]',
497- '[class*="scriptEditorButton"]',
498- ];
488+ return false;
489+ })()
490+ ` ) . catch ( ( ) => false ) ;
491+ if ( apiOk ) return ;
492+
493+ // Step 2: Known DOM selectors — each tried individually (no comma-separated querySelector)
494+ const domSels = [
495+ '[data-name="pine-dialog-button"]' ,
496+ '[data-name="pine-editor-toolbar"]' ,
497+ '[aria-label="Pine Editor"]' ,
498+ '[aria-label="Pine"]' ,
499+ '[class*="scriptEditorButton"]' ,
500+ '[class*="pine-editor-button"]' ,
501+ ] ;
502+ const domOk = await evaluate ( `
503+ (function() {
504+ var sels = ${ JSON . stringify ( domSels ) } ;
499505 for (var i = 0; i < sels.length; i++) {
500506 var b = document.querySelector(sels[i]);
501- if (b && b.offsetParent !== null) { b.click(); return; }
507+ if (b && b.offsetParent !== null) { b.click(); return sels[i] ; }
502508 }
503-
504- // Text scan fallback
505- var btns = document.querySelectorAll('[role="button"], button');
506- for (var j = 0; j < btns.length; j++) {
507- var t = (btns[j].getAttribute('aria-label') || btns[j].textContent || '').trim();
508- if (t === 'Pine' || /pine editor/i.test(t)) { btns[j].click(); return; }
509+ // Text content scan
510+ var all = document.querySelectorAll('[role="button"], button');
511+ for (var j = 0; j < all.length; j++) {
512+ var t = (all[j].getAttribute('aria-label') || all[j].textContent || '').trim();
513+ if (t === 'Pine' || t === 'Pine Editor' || /^pine\s*editor$/i.test(t)) {
514+ all[j].click(); return 'text:' + t;
515+ }
509516 }
517+ return null;
510518 })()
511- ` ) . catch ( ( ) => { } ) ;
519+ ` ) . catch ( ( ) => null ) ;
520+ if ( domOk ) return ;
521+
522+ // Step 3: Keyboard shortcut Alt+P (TradingView default for Pine Editor)
523+ const c = await getClient ( ) ;
524+ await c . Input . dispatchKeyEvent ( { type : 'keyDown' , modifiers : 1 , key : 'p' , code : 'KeyP' , windowsVirtualKeyCode : 80 } ) ;
525+ await c . Input . dispatchKeyEvent ( { type : 'keyUp' , key : 'p' , code : 'KeyP' , windowsVirtualKeyCode : 80 } ) ;
512526}
513527
514528// Ensure Pine Editor is open and Monaco is accessible.
515- // Returns true/false. Maximum wait: ~3 seconds total .
529+ // Smart wait: confirms Monaco model is loaded and editable, not just present .
516530async function ensurePineEditorOpen ( ) {
517- // Check cache first — if we already have Monaco, done immediately
531+ // Fast path: Monaco already cached and alive
518532 const cached = await getMonaco ( ) ;
519533 if ( cached ) return true ;
520534
521535 // Open the editor
522536 await openPineEditor ( ) ;
523537
524- // Poll with a short initial delay, then check every 300ms — max 10 attempts = 3s
525- await new Promise ( r => setTimeout ( r , 600 ) ) ;
526- for ( let i = 0 ; i < 10 ; i ++ ) {
527- const found = await getMonaco ( ) ;
528- if ( found ) return true ;
529- await new Promise ( r => setTimeout ( r , 300 ) ) ;
538+ // Wait for Monaco to become fully ready — not just present but writable.
539+ // TradingView loads Monaco asynchronously; the container appears before the
540+ // editor model is ready. We poll for both presence AND ability to getValue().
541+ for ( let i = 0 ; i < 20 ; i ++ ) {
542+ // Give the animation/load a moment on first few iterations
543+ const delay = i < 3 ? 500 : 300 ;
544+ await new Promise ( r => setTimeout ( r , delay ) ) ;
545+
546+ // Check if Monaco is accessible AND its model is ready
547+ const ready = await evaluate ( `
548+ (function() {
549+ // First: run discovery to populate window.__ot_monaco
550+ if (!window.__ot_monaco || typeof window.__ot_monaco.getValue !== 'function') {
551+ // Quick re-discovery attempt
552+ try {
553+ if (window.monaco && window.monaco.editor) {
554+ var eds = window.monaco.editor.getEditors();
555+ if (eds && eds.length > 0) window.__ot_monaco = eds[eds.length - 1];
556+ }
557+ } catch(e) {}
558+ if (!window.__ot_monaco) {
559+ try {
560+ if (typeof require === 'function') {
561+ var m = require('vs/editor/editor.main');
562+ if (m && m.editor) {
563+ var eds = m.editor.getEditors();
564+ if (eds && eds.length > 0) window.__ot_monaco = eds[eds.length - 1];
565+ }
566+ }
567+ } catch(e) {}
568+ }
569+ if (!window.__ot_monaco) {
570+ var root = document.querySelector('.monaco-editor.pine-editor-monaco')
571+ || document.querySelector('[class*="pine-editor"] .monaco-editor');
572+ if (root) {
573+ var el = root;
574+ for (var up = 0; up < 25 && el; up++, el = el.parentElement) {
575+ var fk = Object.keys(el).find(function(k) { return k.startsWith('__reactFiber$'); });
576+ if (!fk) continue;
577+ var node = el[fk];
578+ for (var d = 0; d < 40 && node; d++, node = node.return) {
579+ var v = ((node.memoizedProps || {}).value || {});
580+ if (v.monacoEnv && v.monacoEnv.editor) {
581+ var eds = v.monacoEnv.editor.getEditors();
582+ if (eds && eds.length > 0) { window.__ot_monaco = eds[eds.length - 1]; break; }
583+ }
584+ }
585+ if (window.__ot_monaco) break;
586+ }
587+ }
588+ }
589+ }
590+
591+ // Now check if Monaco model is actually ready (not just initialized)
592+ if (!window.__ot_monaco) return false;
593+ try {
594+ var model = window.__ot_monaco.getModel();
595+ if (!model) return false;
596+ // Confirm we can read the value — this fails if model not fully loaded
597+ window.__ot_monaco.getValue();
598+ return true;
599+ } catch(e) {
600+ return false;
601+ }
602+ })()
603+ ` ) . catch ( ( ) => false ) ;
604+
605+ if ( ready ) {
606+ _monacoCache = true ;
607+ _monacoLastCheck = Date . now ( ) ;
608+ return true ;
609+ }
530610 }
531611
532- // Editor container exists but Monaco not yet ready — still return true
533- // (setEditorValue has a textarea fallback)
534- const hasContainer = await evaluate ( `
612+ // Last resort: check if editor container is at least visible
613+ const visible = await evaluate ( `
535614 !!(document.querySelector('.monaco-editor.pine-editor-monaco') ||
536- document.querySelector('[class*="pine-editor"] .monaco-editor') ||
537- document.querySelector('[data-name="pine-editor"]'))
615+ document.querySelector('[class*="pine-editor"] .monaco-editor'))
538616 ` ) . catch ( ( ) => false ) ;
539617
540- return hasContainer ;
618+ return visible ;
541619}
542620
543621// Inject source into Monaco using CDP Runtime.callFunctionOn — bypasses
@@ -661,14 +739,19 @@ export async function pineGetSource() {
661739}
662740
663741export async function pineSetSource ( { source } ) {
664- invalidateMonacoCache ( ) ; // force fresh discovery after source change
742+ invalidateMonacoCache ( ) ;
665743 const opened = await ensurePineEditorOpen ( ) ;
666- if ( ! opened ) throw new Error ( 'Could not open Pine Editor. Click the Pine Editor tab in TradingView first.' ) ;
667-
668- await new Promise ( r => setTimeout ( r , 300 ) ) ;
744+ if ( ! opened ) throw new Error ( 'Could not open Pine Editor. Make sure TradingView is open in Chrome and the Pine Editor tab is visible.' ) ;
669745
746+ // ensurePineEditorOpen already confirmed Monaco model is ready — write immediately
670747 const result = await setEditorValue ( source ) ;
671- if ( ! result . ok ) throw new Error ( 'Pine Editor is open but could not write to it. Try clicking inside the editor and retrying.' ) ;
748+ if ( ! result . ok ) {
749+ // One retry after a short wait — editor may have just finished animating
750+ await new Promise ( r => setTimeout ( r , 800 ) ) ;
751+ const retry = await setEditorValue ( source ) ;
752+ if ( ! retry . ok ) throw new Error ( 'Could not write to Pine Editor. The editor opened but the Monaco model is not ready. Try again in a moment.' ) ;
753+ return { success : true , lines : source . split ( '\n' ) . length , method : retry . method + '_retry' } ;
754+ }
672755
673756 return { success : true , lines : source . split ( '\n' ) . length , method : result . method } ;
674757}
@@ -1154,22 +1237,39 @@ export async function uiEvaluate({ code }) {
11541237}
11551238
11561239export async function uiOpenPanel ( { panel } ) {
1157- const selectors = {
1158- 'pine-editor' : '[data-name="pine-dialog-button"], [aria-label="Pine"]' ,
1159- 'strategy-tester' : '[data-name="backtesting-button"], [aria-label="Strategy Tester"]' ,
1160- 'watchlist' : '[data-name="base-watchlist-widget-button"]' ,
1161- 'alerts' : '[data-name="alerts-button"], [aria-label*="Alert"]' ,
1240+ // Map panel names to arrays of selectors — avoids interpolation of quotes
1241+ const selectorMap = {
1242+ 'pine-editor' : [ '[data-name="pine-dialog-button"]' , '[aria-label="Pine"]' , '[data-name="pine-editor-toolbar"]' ] ,
1243+ 'strategy-tester' : [ '[data-name="backtesting-button"]' , '[aria-label="Strategy Tester"]' , '[data-name="strategy-tester-button"]' ] ,
1244+ 'watchlist' : [ '[data-name="base-watchlist-widget-button"]' , '[aria-label="Watchlist"]' ] ,
1245+ 'alerts' : [ '[data-name="alerts-button"]' , '[aria-label="Alerts"]' , '[data-name="alerts-create-button"]' ] ,
11621246 } ;
1163- const sel = selectors [ panel ] ;
1164- if ( ! sel ) throw new Error ( `Unknown panel: ${ panel } . Use: pine-editor, strategy-tester, watchlist, alerts` ) ;
1247+ const sels = selectorMap [ panel ] ;
1248+ if ( ! sels ) throw new Error ( `Unknown panel: ${ panel } . Use: pine-editor, strategy-tester, watchlist, alerts` ) ;
1249+
11651250 const clicked = await evaluate ( `
11661251 (function() {
1167- var el = document.querySelector('${ sel } ');
1168- if (el) { el.click(); return true; }
1169- return false;
1252+ var sels = ${ JSON . stringify ( sels ) } ;
1253+ for (var i = 0; i < sels.length; i++) {
1254+ try {
1255+ var el = document.querySelector(sels[i]);
1256+ if (el && el.offsetParent !== null) { el.click(); return sels[i]; }
1257+ } catch(e) {}
1258+ }
1259+ // Text content fallback
1260+ var labels = { 'pine-editor': /pine/i, 'strategy-tester': /strategy.?tester/i, 'watchlist': /watchlist/i, 'alerts': /alerts/i };
1261+ var label = labels[${ JSON . stringify ( panel ) } ];
1262+ if (label) {
1263+ var btns = document.querySelectorAll('button, [role="button"]');
1264+ for (var j = 0; j < btns.length; j++) {
1265+ var t = btns[j].getAttribute('aria-label') || btns[j].textContent || '';
1266+ if (label.test(t) && btns[j].offsetParent !== null) { btns[j].click(); return 'text:'+t.trim().slice(0,30); }
1267+ }
1268+ }
1269+ return null;
11701270 })()
11711271 ` ) ;
1172- return { success : clicked , panel } ;
1272+ return { success : ! ! clicked , panel, clicked } ;
11731273}
11741274
11751275// ── Batch ──
0 commit comments