Skip to content

Commit 102041c

Browse files
committed
Handle TradingView Pine first-save recovery
1 parent 551d9b2 commit 102041c

File tree

4 files changed

+112
-8
lines changed

4 files changed

+112
-8
lines changed

scripts/test-tradingview-pine-data-workflows.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ test('clipboard-only pine authoring plan rewrites into guarded continuation afte
263263
const saveInspect = freshInspect.continueActions.find((action) => action?.type === 'get_text' && action?.pineEvidenceMode === 'save-status');
264264
assert(saveInspect, 'fresh-script continuation should verify visible save status before applying');
265265
assert.strictEqual(saveInspect.continueOnPineLifecycleState, 'saved-state-verified');
266+
assert(Array.isArray(saveInspect?.continueActionsByPineLifecycleState?.['save-required-before-apply']), 'save verification should branch into a first-save recovery path when TradingView requires a script name');
267+
assert(saveInspect.continueActionsByPineLifecycleState['save-required-before-apply'].some((action) => action?.type === 'type' && /Momentum Confidence/.test(String(action?.text || ''))), 'first-save recovery should derive a script name from the Pine payload');
266268
assert(saveInspect.continueActions.some((action) => action?.type === 'key' && String(action?.key || '').toLowerCase() === 'ctrl+enter'), 'save-verified continuation should add the script to the chart');
267269
assert(saveInspect.continueActions.some((action) => action?.type === 'get_text' && action?.pineEvidenceMode === 'compile-result'), 'save-verified continuation should gather compile-result feedback after add-to-chart');
268270
});

scripts/test-windows-observation-flow.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,8 +1350,9 @@ async function run() {
13501350
assert(execResult.results.some((result) => result?.pineContinuationInjected), 'inspect step should inject continuation actions');
13511351
});
13521352

1353-
await testAsync('safe pine authoring blocks add-to-chart when save evidence is missing', async () => {
1353+
await testAsync('safe pine authoring recovers through first-save naming before add-to-chart', async () => {
13541354
const executed = [];
1355+
let saveStatusReads = 0;
13551356

13561357
const foregroundSequence = [
13571358
{ success: true, hwnd: 777, title: 'TradingView', processName: 'tradingview', windowKind: 'main' },
@@ -1392,24 +1393,38 @@ async function run() {
13921393
};
13931394
}
13941395
if (action?.type === 'get_text' && action?.pineEvidenceMode === 'save-status') {
1396+
saveStatusReads += 1;
13951397
return {
13961398
success: true,
13971399
action: 'get_text',
1398-
message: 'save still required',
1400+
message: saveStatusReads === 1 ? 'save still required' : 'save verified',
13991401
pineStructuredSummary: {
14001402
evidenceMode: 'save-status',
1401-
lifecycleState: 'save-required-before-apply'
1403+
lifecycleState: saveStatusReads === 1 ? 'save-required-before-apply' : 'saved-state-verified'
1404+
}
1405+
};
1406+
}
1407+
if (action?.type === 'get_text' && action?.pineEvidenceMode === 'compile-result') {
1408+
return {
1409+
success: true,
1410+
action: 'get_text',
1411+
message: 'compiled successfully',
1412+
pineStructuredSummary: {
1413+
evidenceMode: 'compile-result',
1414+
lifecycleState: 'apply-result-verified'
14021415
}
14031416
};
14041417
}
14051418
return { success: true, action: action.type, message: 'ok' };
14061419
}
14071420
}));
14081421

1409-
assert.strictEqual(execResult.success, false, 'Execution should stop when save evidence is missing');
1422+
assert.strictEqual(execResult.success, true, 'Execution should recover after the first-save naming flow');
14101423
assert(executed.includes('key:ctrl+s'), 'Save should still be attempted');
1411-
assert(!executed.includes('key:ctrl+enter'), 'Add-to-chart should be blocked without visible save evidence');
1412-
assert(execResult.results.some((result) => /do not add it to the chart yet/i.test(String(result?.error || ''))), 'Failure should explain that save verification blocked add-to-chart');
1424+
assert(executed.includes('type'), 'First-save recovery should type the derived script name');
1425+
assert(executed.includes('key:enter'), 'First-save recovery should confirm the save dialog');
1426+
assert(executed.includes('key:ctrl+enter'), 'Add-to-chart should resume only after save evidence is re-verified');
1427+
assert.strictEqual(saveStatusReads, 2, 'Save status should be checked before and after the first-save recovery');
14131428
});
14141429

14151430
await testAsync('compile-result corruption signal stops pine workflow with grounded editor-target failure', async () => {

src/main/ai-service.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5242,8 +5242,10 @@ async function executeActions(actionData, onAction = null, onScreenshot = null,
52425242
if (
52435243
result.success
52445244
&& effectiveAction.type === 'get_text'
5245-
&& Array.isArray(action.continueActions)
5246-
&& action.continueActions.length > 0
5245+
&& (
5246+
(Array.isArray(action.continueActions) && action.continueActions.length > 0)
5247+
|| (action.continueActionsByPineLifecycleState && typeof action.continueActionsByPineLifecycleState === 'object')
5248+
)
52475249
) {
52485250
const observedPineState = String(result?.pineStructuredSummary?.editorVisibleState || '').trim().toLowerCase();
52495251
const expectedPineState = String(action?.continueOnPineEditorState || '').trim().toLowerCase();
@@ -5275,6 +5277,12 @@ async function executeActions(actionData, onAction = null, onScreenshot = null,
52755277

52765278
const observedPineLifecycleState = String(result?.pineStructuredSummary?.lifecycleState || '').trim().toLowerCase();
52775279
const expectedPineLifecycleState = String(action?.continueOnPineLifecycleState || '').trim().toLowerCase();
5280+
const lifecycleStateContinuations = action?.continueActionsByPineLifecycleState && typeof action.continueActionsByPineLifecycleState === 'object'
5281+
? action.continueActionsByPineLifecycleState
5282+
: null;
5283+
const matchedLifecycleContinuation = lifecycleStateContinuations
5284+
? lifecycleStateContinuations[observedPineLifecycleState] || lifecycleStateContinuations['*'] || null
5285+
: null;
52785286

52795287
if (result.success && observedPineLifecycleState && expectedPineLifecycleState && observedPineLifecycleState === expectedPineLifecycleState) {
52805288
const continuationActions = action.continueActions.map((step) => {
@@ -5291,6 +5299,19 @@ async function executeActions(actionData, onAction = null, onScreenshot = null,
52915299
result.pineContinuationLifecycleState = observedPineLifecycleState;
52925300
result.pineContinuationCount = continuationActions.length;
52935301
}
5302+
} else if (result.success && observedPineLifecycleState && Array.isArray(matchedLifecycleContinuation) && matchedLifecycleContinuation.length > 0) {
5303+
const continuationActions = matchedLifecycleContinuation.map((step) => {
5304+
try {
5305+
return JSON.parse(JSON.stringify(step));
5306+
} catch {
5307+
return { ...step };
5308+
}
5309+
});
5310+
5311+
actionData.actions.splice(i + 1, 0, ...continuationActions);
5312+
result.pineContinuationInjected = true;
5313+
result.pineContinuationLifecycleState = observedPineLifecycleState;
5314+
result.pineContinuationCount = continuationActions.length;
52945315
} else if (result.success && action.haltOnPineLifecycleStateMismatch) {
52955316
const mismatchReasons = action?.pineLifecycleMismatchReasons && typeof action.pineLifecycleMismatchReasons === 'object'
52965317
? action.pineLifecycleMismatchReasons

src/main/tradingview/pine-workflows.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,39 @@ function isPineSaveStep(action) {
155155
|| /\bsave\b.{0,20}\bscript\b/i.test(combined);
156156
}
157157

158+
function extractPineDeclarationTitle(text = '') {
159+
const match = String(text || '').match(/\b(?:indicator|strategy|library)\s*\(\s*["'`](.*?)["'`]/i);
160+
return String(match?.[1] || '').trim();
161+
}
162+
163+
function sanitizePineScriptName(value = '') {
164+
return String(value || '')
165+
.replace(/\s+/g, ' ')
166+
.replace(/[<>:"/\\|?*\u0000-\u001f]+/g, ' ')
167+
.trim()
168+
.slice(0, 120);
169+
}
170+
171+
function inferSafePineScriptName(actions = [], raw = '') {
172+
const source = Array.isArray(actions) ? actions : [];
173+
for (const action of source) {
174+
const type = getNormalizedActionType(action);
175+
if (type === 'type') {
176+
const title = sanitizePineScriptName(extractPineDeclarationTitle(action.text));
177+
if (title) return title;
178+
}
179+
if (type === 'run_command') {
180+
const title = sanitizePineScriptName(extractPineDeclarationTitle(action.command));
181+
if (title) return title;
182+
}
183+
}
184+
185+
const messageTitle = sanitizePineScriptName(String(raw || '').match(/\b(?:called|named)\s+["'`](.*?)["'`]/i)?.[1] || '');
186+
if (messageTitle) return messageTitle;
187+
188+
return 'Liku Pine Script';
189+
}
190+
158191
function shouldAutoAddPineScriptToChart(raw = '', actions = []) {
159192
if (Array.isArray(actions) && actions.some((action) => isPineAddToChartStep(action))) {
160193
return true;
@@ -206,6 +239,8 @@ function buildSafePineAuthoringContinuationSteps(actions = [], intent = {}, raw
206239
return [];
207240
}
208241

242+
const derivedScriptName = inferSafePineScriptName(payloadSteps, raw);
243+
209244
const applyContinuationSteps = [];
210245
if (addToChartSteps.length > 0) {
211246
applyContinuationSteps.push(...addToChartSteps);
@@ -258,6 +293,37 @@ function buildSafePineAuthoringContinuationSteps(actions = [], intent = {}, raw
258293
pineEvidenceMode: 'save-status',
259294
continueOnPineLifecycleState: 'saved-state-verified',
260295
continueActions: applyContinuationSteps,
296+
continueActionsByPineLifecycleState: {
297+
'save-required-before-apply': [
298+
{ type: 'wait', ms: 180 },
299+
{
300+
type: 'type',
301+
text: derivedScriptName,
302+
reason: `Provide a Pine script name in the TradingView first-save flow: ${derivedScriptName}`
303+
},
304+
{ type: 'wait', ms: 120 },
305+
{
306+
type: 'key',
307+
key: 'enter',
308+
reason: 'Confirm the TradingView Pine first-save flow after entering the script name'
309+
},
310+
{ type: 'wait', ms: 450 },
311+
{
312+
type: 'get_text',
313+
text: 'Pine Editor',
314+
reason: 'Re-verify visible Pine save-state evidence after naming the script',
315+
pineEvidenceMode: 'save-status',
316+
continueOnPineLifecycleState: 'saved-state-verified',
317+
continueActions: applyContinuationSteps,
318+
haltOnPineLifecycleStateMismatch: true,
319+
pineLifecycleMismatchReasons: {
320+
'save-required-before-apply': 'TradingView still shows save-required state after naming the script; stop before applying it to the chart.',
321+
'editor-target-corrupt': 'Visible Pine output suggests editor-target corruption during save; stop before applying the script.',
322+
'': 'The Pine save state could not be verified after naming the script; do not add it to the chart yet.'
323+
}
324+
}
325+
]
326+
},
261327
haltOnPineLifecycleStateMismatch: true,
262328
pineLifecycleMismatchReasons: {
263329
'save-required-before-apply': 'Visible save confirmation was not observed after saving the Pine script; do not add it to the chart yet.',

0 commit comments

Comments
 (0)