diff --git a/docs/next-steps.md b/docs/next-steps.md
index 19d5106..07ebe3b 100644
--- a/docs/next-steps.md
+++ b/docs/next-steps.md
@@ -37,3 +37,27 @@ Focused follow-up work for `@knighted/develop`.
- Do not add dependencies without explicit approval.
- Remaining Phase 3 mini-spec (agent implementation prompt):
- "Continue Issue #18 in @knighted/develop from the current baseline where PR filename/path groundwork and Open PR flow are already shipped. Implement the two remaining Phase 3 assistant deliverables. (1) Add mode-aware assistant guidance: when collecting AI context, include explicit policy hints derived from render mode and style mode, and ensure recommendations avoid incompatible patterns (for example, avoid React hook/state guidance in DOM mode unless user explicitly asks for React migration). (2) Add assistant-to-editor apply flow: support structured assistant responses that can propose edits for component and/or styles editors; render these as reviewable actions in the chat drawer, require explicit user confirmation to apply, and support a one-step undo for last applied assistant edit per editor. Keep all AI/BYOT behavior behind the existing browser-only AI feature flag and preserve current token/repo persistence semantics. Do not add dependencies. Validate with npm run lint and targeted Playwright tests covering mode-aware recommendation constraints and apply/undo editor actions."
+
+5. **Phase 2 UX/UI continuation: fixed editor tabs first pass (Component, Styles, App)**
+ - Continue the tabs/editor UX work with a constrained first implementation that supports exactly three editor tabs: Component, Styles, and App.
+ - Do not introduce arbitrary/custom tab names in this pass; treat custom naming as future scope after baseline tab behavior is stable.
+ - Preserve existing runtime behavior and editor content semantics while adding tab switching, active tab indication, and predictable persistence/reset behavior consistent with current app patterns.
+ - Ensure assistant/editor integration remains compatible with this model (edits should target one of the fixed tabs) without expanding to dynamic tab metadata yet.
+ - Suggested implementation prompt:
+ - "Implement Phase 2 UX/UI tab support in @knighted/develop with a fixed first-pass tab model: Component, Styles, and App only (no arbitrary tab names yet). Add a clear tab UI for switching editor panes, preserve existing editor behavior/content wiring, and keep render/lint/typecheck/diagnostics flows working with the selected tab context where relevant. Keep AI/BYOT feature-flag behavior unchanged, maintain CDN-first runtime constraints, and do not add dependencies. Add targeted Playwright coverage for tab switching, default/active tab behavior, and interactions with existing render/style-mode flows. Validate with npm run lint and targeted Playwright tests."
+
+6. **Document implicit App strict-flow behavior (auto render)**
+ - Add a short behavior matrix in docs that explains when implicit App wrapping is allowed versus when users must define `App` explicitly.
+ - Include concrete Component editor examples for each case so reviewer/user expectations are clear.
+ - Suggested example cases to document:
+ - Allowed implicit wrap (standalone top-level JSX, no imports/declarations), for example:
+ - `() as any`
+ - Requires explicit `App` (top-level JSX with declarations/imports), for example:
+ - `const label = 'Hello'`
+ - `const Button = () => `
+ - `() as any`
+ - Recommended explicit pattern, for example:
+ - `const Button = () => `
+ - `const App = () => `
+ - Suggested implementation prompt:
+ - "Document the current implicit App behavior in @knighted/develop for auto-render mode using a compact behavior matrix and concrete component-editor snippets. Clearly distinguish supported implicit wrapping from cases that intentionally require an explicit App (such as top-level JSX mixed with imports/declarations). Keep docs concise, aligned with current runtime behavior, and include at least one positive and one explicit-error example."
diff --git a/playwright/diagnostics.spec.ts b/playwright/diagnostics.spec.ts
index 004051e..bafdf2b 100644
--- a/playwright/diagnostics.spec.ts
+++ b/playwright/diagnostics.spec.ts
@@ -346,7 +346,7 @@ test('clear component diagnostics resets rendered lint-issue status pill', async
await expect(page.getByText('Rendered', { exact: true })).toHaveClass(/status--neutral/)
})
-test('component lint ignores unused App View and render bindings', async ({ page }) => {
+test('component lint ignores only unused App binding', async ({ page }) => {
await waitForInitialRender(page)
await setComponentEditorSource(
@@ -361,20 +361,20 @@ test('component lint ignores unused App View and render bindings', async ({ page
await runComponentLint(page)
await ensureDiagnosticsDrawerOpen(page)
- await expect(page.getByText('No Biome issues found.')).toBeVisible()
const diagnosticsToggle = page.getByRole('button', { name: /^Diagnostics/ })
- await expect(page.getByText('Rendered', { exact: true })).toHaveClass(/status--neutral/)
- await expect(diagnosticsToggle).toHaveText('Diagnostics')
- await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--ok/)
+ await expect(page.getByText(/Rendered \(Lint issues: [1-9]\d*\)/)).toHaveClass(
+ /status--error/,
+ )
+ await expect(diagnosticsToggle).toHaveText(/Diagnostics \([1-9]\d*\)/)
+ await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--error/)
const diagnosticsText = await page.getByRole('complementary').innerText()
+ expect(diagnosticsText).toContain('Biome reported issues.')
expect(diagnosticsText).not.toContain('This variable App is unused')
- expect(diagnosticsText).not.toContain('This variable View is unused')
- expect(diagnosticsText).not.toContain('This variable render is unused')
expect(diagnosticsText).not.toContain('This function App is unused')
- expect(diagnosticsText).not.toContain('This function View is unused')
- expect(diagnosticsText).not.toContain('This function render is unused')
+ expect(diagnosticsText).toContain('This function View is unused')
+ expect(diagnosticsText).toContain('This function render is unused')
})
test('component lint with unresolved issues enters pending diagnostics state while typing', async ({
diff --git a/playwright/github-pr-drawer.spec.ts b/playwright/github-pr-drawer.spec.ts
index 35d125d..4421b9b 100644
--- a/playwright/github-pr-drawer.spec.ts
+++ b/playwright/github-pr-drawer.spec.ts
@@ -8,9 +8,15 @@ import {
connectByotWithSingleRepo,
ensureOpenPrDrawerOpen,
mockRepositoryBranches,
+ setComponentEditorSource,
waitForAppReady,
} from './helpers/app-test-helpers.js'
+const decodeGitHubFileBodyContent = (body: Record) => {
+ const encoded = typeof body.content === 'string' ? body.content : ''
+ return Buffer.from(encoded, 'base64').toString('utf8')
+}
+
test('Open PR drawer confirms and submits component/styles filepaths', async ({
page,
}) => {
@@ -451,3 +457,279 @@ test('Open PR drawer rejects trailing slash file paths', async ({ page }) => {
)
await expect(page.getByRole('dialog')).toBeHidden()
})
+
+test('Open PR drawer include App wrapper checkbox defaults off and resets on reopen', async ({
+ page,
+}) => {
+ await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
+ await connectByotWithSingleRepo(page)
+ await ensureOpenPrDrawerOpen(page)
+
+ const includeWrapperToggle = page.getByLabel(
+ 'Include App wrapper in committed component source',
+ )
+ await expect(includeWrapperToggle).not.toBeChecked()
+
+ await includeWrapperToggle.check()
+ await expect(includeWrapperToggle).toBeChecked()
+
+ await page.getByRole('button', { name: 'Close open pull request drawer' }).click()
+ await ensureOpenPrDrawerOpen(page)
+
+ await expect(includeWrapperToggle).not.toBeChecked()
+})
+
+test('Open PR drawer strips App wrapper from committed component source by default', async ({
+ page,
+}) => {
+ const upsertRequests: Array<{ path: string; body: Record }> = []
+
+ await page.route('https://api.github.com/user/repos**', async route => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([
+ {
+ id: 11,
+ owner: { login: 'knightedcodemonkey' },
+ name: 'develop',
+ full_name: 'knightedcodemonkey/develop',
+ default_branch: 'main',
+ permissions: { push: true },
+ },
+ ]),
+ })
+ })
+
+ await mockRepositoryBranches(page, {
+ 'knightedcodemonkey/develop': ['main', 'release'],
+ })
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/git/ref/**',
+ async route => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ ref: 'refs/heads/main',
+ object: { type: 'commit', sha: 'abc123mainsha' },
+ }),
+ })
+ },
+ )
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/git/refs',
+ async route => {
+ await route.fulfill({
+ status: 201,
+ contentType: 'application/json',
+ body: JSON.stringify({ ref: 'refs/heads/develop/open-pr-app-wrapper' }),
+ })
+ },
+ )
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/contents/**',
+ async route => {
+ const request = route.request()
+ const method = request.method()
+ const path =
+ new URL(request.url()).pathname.split('/contents/')[1] ?? 'unknown-file-path'
+
+ if (method === 'GET') {
+ await route.fulfill({
+ status: 404,
+ contentType: 'application/json',
+ body: JSON.stringify({ message: 'Not Found' }),
+ })
+ return
+ }
+
+ const body = request.postDataJSON() as Record
+ upsertRequests.push({ path: decodeURIComponent(path), body })
+ await route.fulfill({
+ status: 201,
+ contentType: 'application/json',
+ body: JSON.stringify({ commit: { sha: 'commit-sha' } }),
+ })
+ },
+ )
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/pulls',
+ async route => {
+ await route.fulfill({
+ status: 201,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ number: 101,
+ html_url: 'https://github.com/knightedcodemonkey/develop/pull/101',
+ }),
+ })
+ },
+ )
+
+ await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
+ await connectByotWithSingleRepo(page)
+
+ const componentSource = [
+ 'const CounterButton = () => ',
+ 'const App = () => ',
+ ].join('\n')
+
+ await setComponentEditorSource(page, componentSource)
+ await ensureOpenPrDrawerOpen(page)
+
+ await page.getByLabel('Head').fill('develop/repo/editor-sync-without-app')
+ await page.getByRole('button', { name: 'Open PR' }).last().click()
+ await page.getByRole('dialog').getByRole('button', { name: 'Open PR' }).click()
+
+ await expect(
+ page.getByRole('status', { name: 'Open pull request status', includeHidden: true }),
+ ).toContainText(
+ 'Pull request opened: https://github.com/knightedcodemonkey/develop/pull/101',
+ )
+
+ const componentUpserts = upsertRequests.filter(request =>
+ request.path.endsWith('/App.jsx'),
+ )
+
+ expect(componentUpserts).toHaveLength(1)
+
+ const strippedComponentSource = decodeGitHubFileBodyContent(componentUpserts[0].body)
+
+ expect(strippedComponentSource).toContain('const CounterButton = () =>')
+ expect(strippedComponentSource).not.toContain('const App = () =>')
+})
+
+test('Open PR drawer includes App wrapper in committed source when toggled on', async ({
+ page,
+}) => {
+ const upsertRequests: Array<{ path: string; body: Record }> = []
+
+ await page.route('https://api.github.com/user/repos**', async route => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([
+ {
+ id: 11,
+ owner: { login: 'knightedcodemonkey' },
+ name: 'develop',
+ full_name: 'knightedcodemonkey/develop',
+ default_branch: 'main',
+ permissions: { push: true },
+ },
+ ]),
+ })
+ })
+
+ await mockRepositoryBranches(page, {
+ 'knightedcodemonkey/develop': ['main', 'release'],
+ })
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/git/ref/**',
+ async route => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ ref: 'refs/heads/main',
+ object: { type: 'commit', sha: 'abc123mainsha' },
+ }),
+ })
+ },
+ )
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/git/refs',
+ async route => {
+ await route.fulfill({
+ status: 201,
+ contentType: 'application/json',
+ body: JSON.stringify({ ref: 'refs/heads/develop/open-pr-app-wrapper' }),
+ })
+ },
+ )
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/contents/**',
+ async route => {
+ const request = route.request()
+ const method = request.method()
+ const path =
+ new URL(request.url()).pathname.split('/contents/')[1] ?? 'unknown-file-path'
+
+ if (method === 'GET') {
+ await route.fulfill({
+ status: 404,
+ contentType: 'application/json',
+ body: JSON.stringify({ message: 'Not Found' }),
+ })
+ return
+ }
+
+ const body = request.postDataJSON() as Record
+ upsertRequests.push({ path: decodeURIComponent(path), body })
+ await route.fulfill({
+ status: 201,
+ contentType: 'application/json',
+ body: JSON.stringify({ commit: { sha: 'commit-sha' } }),
+ })
+ },
+ )
+
+ await page.route(
+ 'https://api.github.com/repos/knightedcodemonkey/develop/pulls',
+ async route => {
+ await route.fulfill({
+ status: 201,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ number: 101,
+ html_url: 'https://github.com/knightedcodemonkey/develop/pull/101',
+ }),
+ })
+ },
+ )
+
+ await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
+ await connectByotWithSingleRepo(page)
+
+ await setComponentEditorSource(
+ page,
+ [
+ 'const CounterButton = () => ',
+ 'const App = () => ',
+ ].join('\n'),
+ )
+ await ensureOpenPrDrawerOpen(page)
+
+ const includeWrapperToggle = page.getByLabel(
+ 'Include App wrapper in committed component source',
+ )
+ await includeWrapperToggle.check()
+
+ await page.getByLabel('Head').fill('develop/repo/editor-sync-with-app')
+ await page.getByRole('button', { name: 'Open PR' }).last().click()
+ await page.getByRole('dialog').getByRole('button', { name: 'Open PR' }).click()
+
+ await expect(
+ page.getByRole('status', { name: 'Open pull request status', includeHidden: true }),
+ ).toContainText(
+ 'Pull request opened: https://github.com/knightedcodemonkey/develop/pull/101',
+ )
+
+ const componentUpserts = upsertRequests.filter(request =>
+ request.path.endsWith('/App.jsx'),
+ )
+
+ expect(componentUpserts).toHaveLength(1)
+
+ const fullComponentSource = decodeGitHubFileBodyContent(componentUpserts[0].body)
+ expect(fullComponentSource).toContain('const CounterButton = () =>')
+ expect(fullComponentSource).toContain('const App = () =>')
+})
diff --git a/playwright/rendering-modes.spec.ts b/playwright/rendering-modes.spec.ts
index 051b6f9..7e5f031 100644
--- a/playwright/rendering-modes.spec.ts
+++ b/playwright/rendering-modes.spec.ts
@@ -190,6 +190,244 @@ test('requires render button when auto render is disabled', async ({ page }) =>
await expect(page.locator('#preview-host pre')).toHaveCount(0)
})
+test('clears preview when auto render is toggled', async ({ page }) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+
+ const autoRenderToggle = page.getByLabel('Auto render')
+
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toHaveCount(1)
+
+ await autoRenderToggle.uncheck()
+
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toHaveCount(0)
+ await expect(page.locator('#preview-host pre')).toHaveCount(0)
+})
+
+test('shows App-only error when auto render is disabled and App is missing', async ({
+ page,
+}) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+
+ const autoRenderToggle = page.getByLabel('Auto render')
+ const renderButton = page.getByRole('button', { name: 'Render' })
+
+ await autoRenderToggle.uncheck()
+ await setComponentEditorSource(
+ page,
+ 'const Button = () => ',
+ )
+
+ await renderButton.click()
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
+ await expect(page.locator('#preview-host pre')).toContainText(
+ 'Expected a function or const named App.',
+ )
+})
+
+test('auto render implicitly wraps source with App in dom and react modes', async ({
+ page,
+}) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ 'const Button = () => ',
+ )
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toContainText('implicit app dom')
+
+ await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
+ await setComponentEditorSource(
+ page,
+ 'const Button = () => ',
+ )
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toContainText('implicit app react')
+})
+
+test('auto render implicit App includes multiple component declarations', async ({
+ page,
+}) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ [
+ 'const OtherButton = () => ',
+ 'const Button = () => ',
+ ].join('\n'),
+ )
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toHaveCount(2)
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toContainText(['bar', 'foo'])
+})
+
+test('auto render does not treat lowercase helpers as implicit components', async ({
+ page,
+}) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ [
+ 'const helper = () => ',
+ 'function render() { return }',
+ ].join('\n'),
+ )
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
+ await expect(page.locator('#preview-host pre')).toContainText(
+ 'Expected a function or const named App.',
+ )
+})
+
+test('auto render wraps standalone JSX with trailing semicolon and comment', async ({
+ page,
+}) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ '() as any; // trailing',
+ )
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toContainText('implicit app from jsx expression')
+})
+
+test('auto render requires explicit App for declarations plus top-level JSX expression', async ({
+ page,
+}) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ [
+ "const label = 'kept declarations'",
+ 'const Button = () => ',
+ '() as any',
+ ].join('\n'),
+ )
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
+ await expect(page.locator('#preview-host pre')).toContainText(
+ 'Top-level JSX with declarations or imports requires an explicit App component.',
+ )
+})
+
+test('renders export default arrow component when auto render is disabled', async ({
+ page,
+}) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+ await page.getByLabel('Auto render').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ 'export default () => ',
+ )
+
+ await page.getByRole('button', { name: 'Render' }).click()
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button'),
+ ).toContainText('default export arrow')
+})
+
+test('renders export default class component in react mode', async ({ page }) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+ await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
+ await page.getByLabel('Auto render').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ [
+ "import React from 'react'",
+ 'export default class extends React.Component {',
+ ' render() {',
+ ' return ',
+ ' }',
+ '}',
+ ].join('\n'),
+ )
+
+ await page.getByRole('button', { name: 'Render' }).click()
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
+ const previewButtons = page
+ .getByRole('region', { name: 'Preview output' })
+ .getByRole('button')
+ await expect(previewButtons.first()).toContainText('default export class')
+})
+
+test('supports export default App without redeclaration', async ({ page }) => {
+ await waitForInitialRender(page)
+
+ await ensurePanelToolsVisible(page, 'component')
+ await page.getByLabel('ShadowRoot').uncheck()
+ await page.getByLabel('Auto render').uncheck()
+
+ await setComponentEditorSource(
+ page,
+ [
+ 'function App() {',
+ ' return ',
+ '}',
+ 'export default App',
+ ].join('\n'),
+ )
+
+ await page.getByRole('button', { name: 'Render' }).click()
+
+ await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
+ await expect(
+ page.getByRole('region', { name: 'Preview output' }).getByRole('button').first(),
+ ).toContainText('export default App')
+})
+
test('persists layout and theme across reload', async ({ page }) => {
await waitForInitialRender(page)
diff --git a/src/app.js b/src/app.js
index 1fd0cae..5414456 100644
--- a/src/app.js
+++ b/src/app.js
@@ -16,6 +16,8 @@ import { createLintDiagnosticsController } from './modules/lint-diagnostics.js'
import { createPreviewBackgroundController } from './modules/preview-background.js'
import { createRenderRuntimeController } from './modules/render-runtime.js'
import { createTypeDiagnosticsController } from './modules/type-diagnostics.js'
+import { collectTopLevelDeclarations } from './modules/jsx-top-level-declarations.js'
+import { ensureJsxTransformSource } from './modules/jsx-transform-runtime.js'
const statusNode = document.getElementById('status')
const appGrid = document.querySelector('.app-grid')
@@ -48,6 +50,7 @@ const githubPrComponentPath = document.getElementById('github-pr-component-path'
const githubPrStylesPath = document.getElementById('github-pr-styles-path')
const githubPrTitle = document.getElementById('github-pr-title')
const githubPrBody = document.getElementById('github-pr-body')
+const githubPrIncludeAppWrapper = document.getElementById('github-pr-include-app-wrapper')
const githubPrSubmit = document.getElementById('github-pr-submit')
const viewControlsToggle = document.getElementById('view-controls-toggle')
const viewControlsDrawer = document.getElementById('view-controls-drawer')
@@ -622,6 +625,18 @@ const getCurrentWritableRepositories = () =>
const setCurrentSelectedRepository = fullName =>
byotControls.setSelectedRepository(fullName)
+const getTopLevelDeclarations = async source => {
+ if (typeof source !== 'string' || !source.trim()) {
+ return []
+ }
+
+ const transformJsxSource = await ensureJsxTransformSource({
+ cdnImports,
+ importFromCdnWithFallback,
+ })
+ return collectTopLevelDeclarations({ source, transformJsxSource })
+}
+
chatDrawerController = createGitHubChatDrawer({
featureEnabled: aiAssistantFeatureEnabled,
toggleButton: aiChatToggle,
@@ -660,6 +675,7 @@ prDrawerController = createGitHubPrDrawer({
stylesPathInput: githubPrStylesPath,
prTitleInput: githubPrTitle,
prBodyInput: githubPrBody,
+ includeAppWrapperToggle: githubPrIncludeAppWrapper,
submitButton: githubPrSubmit,
statusNode: githubPrStatus,
getToken: getCurrentGitHubToken,
@@ -668,6 +684,7 @@ prDrawerController = createGitHubPrDrawer({
setSelectedRepository: setCurrentSelectedRepository,
getComponentSource: () => getJsxSource(),
getStylesSource: () => getCssSource(),
+ getTopLevelDeclarations,
getDrawerSide: () => {
const layout = getCurrentLayout()
return layout === 'preview-left' ? 'left' : 'right'
@@ -1030,6 +1047,7 @@ renderRuntime = createRenderRuntimeController({
renderMode,
styleMode,
shadowToggle,
+ isAutoRenderEnabled: () => autoRenderToggle.checked,
getCssSource: () => getCssSource(),
getJsxSource: () => getJsxSource(),
getPreviewHost: () => previewHost,
@@ -1214,6 +1232,7 @@ styleMode.addEventListener('change', () => {
})
shadowToggle.addEventListener('change', maybeRender)
autoRenderToggle.addEventListener('change', () => {
+ renderRuntime.clearPreview()
updateRenderButtonVisibility()
if (autoRenderToggle.checked) {
renderPreview()
diff --git a/src/index.html b/src/index.html
index 89cd3ba..dc59bf3 100644
--- a/src/index.html
+++ b/src/index.html
@@ -785,6 +785,14 @@ Open Pull Request
/>
+
+