diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.grok/skills/impeccable/scripts/live-browser.js b/.grok/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.grok/skills/impeccable/scripts/live-browser.js +++ b/.grok/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.hermes/skills/impeccable/scripts/live-browser.js b/.hermes/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.hermes/skills/impeccable/scripts/live-browser.js +++ b/.hermes/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/.vibe/skills/impeccable/scripts/live-browser.js b/.vibe/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/.vibe/skills/impeccable/scripts/live-browser.js +++ b/.vibe/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; } diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index da026e255..c32101b12 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -6216,6 +6216,71 @@ return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); } + function sourceHasSessionWrapper(text, sessionId) { + const src = String(text || ''); + return src.indexOf('data-impeccable-variants="' + sessionId + '"') !== -1 + || src.indexOf("data-impeccable-variants='" + sessionId + "'") !== -1 + || src.indexOf('impeccable-variants-start ' + sessionId) !== -1; + } + + /** + * Orphan probe for JSX targets (#439 + #454). An unmounted wrapper and a + * wrapper deleted from source look identical in the DOM, and only the second + * is an orphan, so the DOM alone cannot decide. #454 forbids parsing or + * injecting raw JSX; reading the file as plain text and matching the session + * marker honors that, because no DOM is ever built from what comes back. + * Marker present means the component is simply not mounted right now (a + * closed modal, another route) and the variant observer keeps waiting. + * Marker absent after the same retry budget the HTML path uses means the + * file was edited out from under the session, which no reload, HMR push, or + * server restart can repair, so the session self-discards and hands the + * surface back to the picker. + */ + function probeJsxWrapperForOrphan(filePath, sessionId, opts) { + const attempt = opts._orphanAttempt || 0; + const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); + const stillActive = () => sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING'); + const retryLater = () => { + setTimeout(() => { + if (!stillActive()) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + }; + // Discarding is durable (the session moves to the discarded phase and the + // picker replaces it), so it needs evidence that the wrapper is gone: a + // read that answers without the marker, or a 404 (the file itself was + // renamed or deleted). Either kind retries on the shared budget first. + // A read that fails for any other reason (the server briefly away, a + // transient fetch error) says nothing about the wrapper; after the budget + // the session is kept, the user told, and the next event retries. + const onNoWrapper = (reason) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + discardOrphanedSession(reason); + }; + const onUnreadable = (detail) => { + if (!stillActive()) return; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { retryLater(); return; } + console.warn('[impeccable] Could not read source to check the variant wrapper; keeping the session: ' + detail); + showToast('Could not read the source file to check this session; it stays open and is checked again on the next event.', 5500); + }; + fetch(url) + .then(r => { if (!r.ok) throw new Error('source read failed: ' + r.status); return r.text(); }) + .then(text => { + if (!stillActive()) return; + if (sourceHasSessionWrapper(text, sessionId)) return; + onNoWrapper('variant wrapper missing from source'); + }) + .catch(err => { + const detail = err && err.message ? err.message : 'fetch failed'; + if (/source read failed: 404$/.test(detail)) { + onNoWrapper('source file missing (404) while checking for the variant wrapper'); + return; + } + onUnreadable(detail); + }); + } + function completeSourceInjection(wrapper, sessionId, opts) { recoveryWaitingForAnchor = false; if (pendingVariantAnchorRetryObserver) { @@ -6326,14 +6391,7 @@ return; } if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { - const attempt = opts._orphanAttempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - setTimeout(() => { - if (sessionId !== currentSessionId) return; - if (state !== 'GENERATING' && state !== 'CYCLING') return; - injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - } + probeJsxWrapperForOrphan(filePath, sessionId, opts); } return; }