diff --git a/docs/next-steps.md b/docs/next-steps.md index de4148e..19d5106 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -19,19 +19,21 @@ Focused follow-up work for `@knighted/develop`. - Suggested implementation prompt: - "Add a deterministic E2E execution mode for `@knighted/develop` that serves pinned runtime artifacts locally (instead of live CDN fetches) and wire it into CI as a required check on every PR. Keep a separate lightweight CDN-smoke E2E check for real-network coverage. Validate with `npm run lint`, deterministic Playwright PR checks, and one CDN-smoke Playwright run." -4. **Issue #18 continuation (resume from Phase 3)** +4. **Issue #18 continuation (finish remaining Phase 3 scope)** - Current rollout status: - Phase 0 complete: feature flag + scaffolding. - Phase 1 complete: BYOT token flow, localStorage persistence, writable repo discovery/filtering. - Phase 2 complete: separate AI chat drawer UX, streaming-first responses with non-stream fallback, selected repository context plumbing, and README fine-grained PAT setup links. - - Implement the next slice first (Phase 3): + - Phase 3 partially complete: PR-prep filename/path groundwork landed via the Open PR drawer with repository-scoped persistence and stricter path validation. + - Phase 4 complete: open PR flow from editor content (branch creation, file upserts, PR creation), confirmation UX, loading/success states, and toast feedback. + - Post-implementation hardening complete: traversal/path validation edge cases, trailing-slash rejection, writable-repo select reset behavior during loading/error states, and a JS-driven Playwright readiness check. + - Implement the next slice (remaining Phase 3 assistant features): - Add mode-aware recommendation behavior so the assistant strongly adapts suggestions to current render mode and style mode. - Add an editor update workflow where the assistant can propose structured edits and the user can apply to Component and Styles editors with explicit confirmation. - - Add filename groundwork for upcoming PR flows by allowing user-defined Component and Styles file names, persisted per selected repository. - Keep behavior and constraints aligned with current implementation: - Keep everything behind the existing browser-only AI feature flag. - Preserve BYOT token semantics (localStorage persistence until user deletes). - Keep CDN-first runtime behavior and existing fallback model. - Do not add dependencies without explicit approval. - - Phase 3 mini-spec (agent implementation prompt): - - "Continue Issue #18 in @knighted/develop from the current Phase 2 baseline. Implement Phase 3 with three 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. (3) Add PR-prep filename metadata: introduce user-editable fields for Component filename and Styles filename in AI controls, validate simple safe filename format, and persist/reload values scoped to selected repository so Phase 4 PR write flow can reuse them. 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, apply/undo editor actions, and repository-scoped filename persistence." + - 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." diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index d6a4b58..14a2a6f 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -17,10 +17,21 @@ type ChatRequestBody = { stream?: boolean } +type CreateRefRequestBody = { + ref?: string + sha?: string +} + +type PullRequestCreateBody = { + head?: string + base?: string +} + const waitForAppReady = async (page: Page, path = appEntryPath) => { await page.goto(path) await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible() await expect(page.locator('#cdn-loading')).toHaveAttribute('hidden', '') + await expect.poll(() => page.locator('#status').textContent()).not.toBe('Idle') } const waitForInitialRender = async (page: Page) => { @@ -124,6 +135,18 @@ const ensureAiChatDrawerOpen = async (page: Page) => { await expect(page.locator('#ai-chat-drawer')).toBeVisible() } +const ensureOpenPrDrawerOpen = async (page: Page) => { + const toggle = page.locator('#github-pr-toggle') + await expect(toggle).toBeEnabled({ timeout: 60_000 }) + const isExpanded = await toggle.getAttribute('aria-expanded') + + if (isExpanded !== 'true') { + await toggle.click() + } + + await expect(page.locator('#github-pr-drawer')).toBeVisible() +} + const connectByotWithSingleRepo = async (page: Page) => { await page.route('https://api.github.com/user/repos**', async route => { await route.fulfill({ @@ -144,9 +167,8 @@ const connectByotWithSingleRepo = async (page: Page) => { await page.locator('#github-token-input').fill('github_pat_fake_chat_1234567890') await page.locator('#github-token-add').click() - await expect(page.locator('#github-repo-select')).toHaveValue( - 'knightedcodemonkey/develop', - ) + await expect(page.locator('#status')).toHaveText('Loaded 1 writable repositories') + await expect(page.locator('#github-pr-toggle')).toBeVisible() } const expectCollapseButtonState = async ( @@ -187,6 +209,8 @@ test('BYOT controls stay hidden when feature flag is disabled', async ({ page }) await expect(byotControls).toBeHidden() await expect(page.locator('#ai-chat-toggle')).toBeHidden() await expect(page.locator('#ai-chat-drawer')).toBeHidden() + await expect(page.locator('#github-pr-toggle')).toBeHidden() + await expect(page.locator('#github-pr-drawer')).toBeHidden() }) test('BYOT controls render when feature flag is enabled by query param', async ({ @@ -199,6 +223,7 @@ test('BYOT controls render when feature flag is enabled by query param', async ( await expect(page.locator('#github-token-input')).toBeVisible() await expect(page.locator('#github-token-add')).toBeVisible() await expect(page.locator('#github-ai-controls #ai-chat-toggle')).toBeHidden() + await expect(page.locator('#github-ai-controls #github-pr-toggle')).toBeHidden() }) test('GitHub token info panel reflects missing and present token states', async ({ @@ -476,6 +501,8 @@ test('AI chat falls back to non-streaming response when streaming fails', async }) test('BYOT remembers selected repository across reloads', async ({ page }) => { + test.setTimeout(90_000) + await page.route('https://api.github.com/user/repos**', async route => { await route.fulfill({ status: 200, @@ -506,7 +533,9 @@ test('BYOT remembers selected repository across reloads', async ({ page }) => { await page.locator('#github-token-input').fill('github_pat_fake_1234567890') await page.locator('#github-token-add').click() - const repoSelect = page.locator('#github-repo-select') + await ensureOpenPrDrawerOpen(page) + + const repoSelect = page.locator('#github-pr-repo-select') await expect(repoSelect).toBeEnabled() await expect(page.locator('#status')).toHaveText('Loaded 2 writable repositories') @@ -515,11 +544,224 @@ test('BYOT remembers selected repository across reloads', async ({ page }) => { await page.reload() await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible() + await expect(page.locator('#status')).toHaveText('Loaded 2 writable repositories', { + timeout: 60_000, + }) await expect(page.locator('#github-token-add')).toBeHidden() await expect(page.locator('#github-token-delete')).toBeVisible() + await ensureOpenPrDrawerOpen(page) await expect(repoSelect).toHaveValue('knightedcodemonkey/develop') }) +test('Open PR drawer confirms and submits component/styles filepaths', async ({ + page, +}) => { + let createdRefBody: CreateRefRequestBody | null = null + const upsertRequests: Array<{ path: string; body: Record }> = [] + let pullRequestBody: PullRequestCreateBody | null = null + + 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 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 => { + createdRefBody = route.request().postDataJSON() as CreateRefRequestBody + await route.fulfill({ + status: 201, + contentType: 'application/json', + body: JSON.stringify({ ref: 'refs/heads/develop/open-pr-test' }), + }) + }, + ) + + await page.route( + 'https://api.github.com/repos/knightedcodemonkey/develop/contents/**', + async route => { + const request = route.request() + const method = request.method() + const url = request.url() + const path = new URL(url).pathname.split('/contents/')[1] ?? '' + + 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 => { + pullRequestBody = route.request().postDataJSON() as PullRequestCreateBody + await route.fulfill({ + status: 201, + contentType: 'application/json', + body: JSON.stringify({ + number: 42, + html_url: 'https://github.com/knightedcodemonkey/develop/pull/42', + }), + }) + }, + ) + + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await connectByotWithSingleRepo(page) + await ensureOpenPrDrawerOpen(page) + + await page.locator('#github-pr-head-branch').fill('Develop/Open-Pr-Test') + await page.locator('#github-pr-component-path').fill('examples/component/App.tsx') + await page.locator('#github-pr-styles-path').fill('examples/styles/app.css') + await page.locator('#github-pr-title').fill('Apply editor updates from develop') + await page + .locator('#github-pr-body') + .fill('Generated from editor content in @knighted/develop.') + + await page.locator('#github-pr-submit').click() + + const dialog = page.locator('#clear-confirm-dialog') + await expect(dialog).toHaveAttribute('open', '') + await expect(page.locator('#clear-confirm-title')).toHaveText( + 'Open pull request with editor content?', + ) + await expect(page.locator('#clear-confirm-copy')).toContainText( + 'Component file path: examples/component/App.tsx', + ) + await expect(page.locator('#clear-confirm-copy')).toContainText( + 'Styles file path: examples/styles/app.css', + ) + + await dialog.getByRole('button', { name: 'Open PR' }).click() + + await expect(page.locator('#github-pr-status')).toContainText( + 'Pull request opened: https://github.com/knightedcodemonkey/develop/pull/42', + ) + + const createdRefPayload = createdRefBody as CreateRefRequestBody | null + const pullRequestPayload = pullRequestBody as PullRequestCreateBody | null + + expect(createdRefPayload?.ref).toBe('refs/heads/Develop/Open-Pr-Test') + expect(createdRefPayload?.sha).toBe('abc123mainsha') + + expect(upsertRequests).toHaveLength(2) + expect(upsertRequests[0]?.path).toBe('examples/component/App.tsx') + expect(upsertRequests[1]?.path).toBe('examples/styles/app.css') + expect(pullRequestPayload?.head).toBe('Develop/Open-Pr-Test') + expect(pullRequestPayload?.base).toBe('main') + + await ensureOpenPrDrawerOpen(page) + await expect(page.locator('#github-pr-component-path')).toHaveValue( + 'examples/component/App.tsx', + ) + await expect(page.locator('#github-pr-styles-path')).toHaveValue( + 'examples/styles/app.css', + ) + await expect(page.locator('#github-pr-base-branch')).toHaveValue('main') + + await expect(page.locator('#github-pr-head-branch')).toHaveValue( + /^develop\/develop\/editor-sync-/, + ) + await expect(page.locator('#github-pr-head-branch')).not.toHaveValue( + 'Develop/Open-Pr-Test', + ) + await expect(page.locator('#github-pr-title')).toHaveValue( + 'Apply component and styles edits to knightedcodemonkey/develop', + ) + await expect(page.locator('#github-pr-body')).toHaveValue( + [ + 'This PR was created from @knighted/develop editor content.', + '', + '- Component source -> examples/component/App.tsx', + '- Styles source -> examples/styles/app.css', + ].join('\n'), + ) +}) + +test('Open PR drawer validates unsafe filepaths', async ({ page }) => { + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await connectByotWithSingleRepo(page) + await ensureOpenPrDrawerOpen(page) + + await page.locator('#github-pr-component-path').fill('../outside/App.tsx') + await page.locator('#github-pr-submit').click() + + await expect(page.locator('#github-pr-status')).toContainText( + 'Component path: File path cannot include parent directory traversal.', + ) + await expect(page.locator('#clear-confirm-dialog')).not.toHaveAttribute('open', '') +}) + +test('Open PR drawer allows dotted file segments that are not traversal', async ({ + page, +}) => { + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await connectByotWithSingleRepo(page) + await ensureOpenPrDrawerOpen(page) + + await page.locator('#github-pr-component-path').fill('docs/v1.0..v1.1/App.tsx') + await page.locator('#github-pr-styles-path').fill('styles/foo..bar.css') + await page.locator('#github-pr-submit').click() + + await expect(page.locator('#clear-confirm-dialog')).toHaveAttribute('open', '') + await expect(page.locator('#github-pr-status')).not.toContainText( + 'File path cannot include parent directory traversal.', + ) +}) + +test('Open PR drawer rejects trailing slash file paths', async ({ page }) => { + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await connectByotWithSingleRepo(page) + await ensureOpenPrDrawerOpen(page) + + await page.locator('#github-pr-component-path').fill('src/components/') + await page.locator('#github-pr-submit').click() + + await expect(page.locator('#github-pr-status')).toContainText( + 'Component path: File path must include a filename (no trailing slash).', + ) + await expect(page.locator('#clear-confirm-dialog')).not.toHaveAttribute('open', '') +}) + test('renders default playground preview', async ({ page }) => { await waitForInitialRender(page) diff --git a/src/app.js b/src/app.js index d1b4d70..fe3facc 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,7 @@ import { createDiagnosticsUiController } from './modules/diagnostics-ui.js' import { isAiAssistantFeatureEnabled } from './modules/feature-flags.js' import { createGitHubChatDrawer } from './modules/github-chat-drawer.js' import { createGitHubByotControls } from './modules/github-byot-controls.js' +import { createGitHubPrDrawer } from './modules/github-pr-drawer.js' import { createLayoutThemeController } from './modules/layout-theme.js' import { createLintDiagnosticsController } from './modules/lint-diagnostics.js' import { createPreviewBackgroundController } from './modules/preview-background.js' @@ -24,8 +25,6 @@ const githubTokenInfo = document.getElementById('github-token-info') const githubTokenInfoPanel = document.getElementById('github-token-info-panel') const githubTokenAdd = document.getElementById('github-token-add') const githubTokenDelete = document.getElementById('github-token-delete') -const githubRepoWrap = document.getElementById('github-repo-wrap') -const githubRepoSelect = document.getElementById('github-repo-select') const aiChatToggle = document.getElementById('ai-chat-toggle') const aiChatDrawer = document.getElementById('ai-chat-drawer') const aiChatClose = document.getElementById('ai-chat-close') @@ -38,6 +37,18 @@ const aiChatStatus = document.getElementById('ai-chat-status') const aiChatRate = document.getElementById('ai-chat-rate') const aiChatRepository = document.getElementById('ai-chat-repository') const aiChatMessages = document.getElementById('ai-chat-messages') +const githubPrToggle = document.getElementById('github-pr-toggle') +const githubPrDrawer = document.getElementById('github-pr-drawer') +const githubPrClose = document.getElementById('github-pr-close') +const githubPrStatus = document.getElementById('github-pr-status') +const githubPrRepoSelect = document.getElementById('github-pr-repo-select') +const githubPrBaseBranch = document.getElementById('github-pr-base-branch') +const githubPrHeadBranch = document.getElementById('github-pr-head-branch') +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 githubPrSubmit = document.getElementById('github-pr-submit') const viewControlsToggle = document.getElementById('view-controls-toggle') const viewControlsDrawer = document.getElementById('view-controls-drawer') const aiControlsToggle = document.getElementById('ai-controls-toggle') @@ -71,6 +82,7 @@ const diagnosticsClearAll = document.getElementById('diagnostics-clear-all') const diagnosticsComponent = document.getElementById('diagnostics-component') const diagnosticsStyles = document.getElementById('diagnostics-styles') const cdnLoading = document.getElementById('cdn-loading') +const appToast = document.getElementById('app-toast') const previewBgColorInput = document.getElementById('preview-bg-color') const clearConfirmDialog = document.getElementById('clear-confirm-dialog') const clearConfirmTitle = document.getElementById('clear-confirm-title') @@ -89,9 +101,32 @@ let renderRuntime = null let pendingClearAction = null let suppressEditorChangeSideEffects = false let hasAppliedReactModeDefault = false +let appToastDismissTimer = null const clipboardSupported = Boolean(navigator.clipboard?.writeText) const aiAssistantFeatureEnabled = isAiAssistantFeatureEnabled() +const showAppToast = message => { + if (!(appToast instanceof HTMLElement)) { + return + } + + if (appToastDismissTimer) { + clearTimeout(appToastDismissTimer) + appToastDismissTimer = null + } + + appToast.textContent = message + appToast.hidden = false + appToast.dataset.open = 'true' + + appToastDismissTimer = setTimeout(() => { + appToast.dataset.open = 'false' + appToastDismissTimer = setTimeout(() => { + appToast.hidden = true + }, 190) + }, 4500) +} + const previewBackground = createPreviewBackgroundController({ previewBgColorInput, getPreviewHost: () => previewHost, @@ -498,6 +533,7 @@ const { const githubAiContextState = { token: null, selectedRepository: null, + writableRepositories: [], } let chatDrawerController = { @@ -507,17 +543,29 @@ let chatDrawerController = { dispose: () => {}, } +let prDrawerController = { + setOpen: () => {}, + setSelectedRepository: () => {}, + setToken: () => {}, + syncRepositories: () => {}, + dispose: () => {}, +} + const syncAiChatTokenVisibility = token => { const hasToken = typeof token === 'string' && token.trim().length > 0 if (hasToken) { aiChatToggle?.removeAttribute('hidden') + githubPrToggle?.removeAttribute('hidden') return } aiChatToggle?.setAttribute('hidden', '') aiChatToggle?.setAttribute('aria-expanded', 'false') + githubPrToggle?.setAttribute('hidden', '') + githubPrToggle?.setAttribute('aria-expanded', 'false') chatDrawerController.setOpen(false) + prDrawerController.setOpen(false) } const byotControls = createGitHubByotControls({ @@ -527,11 +575,16 @@ const byotControls = createGitHubByotControls({ tokenInfoButton: githubTokenInfo, tokenAddButton: githubTokenAdd, tokenDeleteButton: githubTokenDelete, - repoSelect: githubRepoSelect, - repoWrap: githubRepoWrap, onRepositoryChange: repository => { githubAiContextState.selectedRepository = repository chatDrawerController.setSelectedRepository(repository) + prDrawerController.setSelectedRepository(repository) + }, + onWritableRepositoriesChange: ({ repositories }) => { + githubAiContextState.writableRepositories = Array.isArray(repositories) + ? [...repositories] + : [] + prDrawerController.syncRepositories() }, onTokenDeleteRequest: onConfirm => { confirmAction({ @@ -547,18 +600,28 @@ const byotControls = createGitHubByotControls({ githubAiContextState.token = token syncAiChatTokenVisibility(token) chatDrawerController.setToken(token) + prDrawerController.setToken(token) }, setStatus, }) githubAiContextState.selectedRepository = byotControls.getSelectedRepository() githubAiContextState.token = byotControls.getToken() +githubAiContextState.writableRepositories = byotControls.getWritableRepositories() const getCurrentGitHubToken = () => githubAiContextState.token ?? byotControls.getToken() const getCurrentSelectedRepository = () => githubAiContextState.selectedRepository ?? byotControls.getSelectedRepository() +const getCurrentWritableRepositories = () => + githubAiContextState.writableRepositories.length > 0 + ? [...githubAiContextState.writableRepositories] + : byotControls.getWritableRepositories() + +const setCurrentSelectedRepository = fullName => + byotControls.setSelectedRepository(fullName) + chatDrawerController = createGitHubChatDrawer({ featureEnabled: aiAssistantFeatureEnabled, toggleButton: aiChatToggle, @@ -585,6 +648,45 @@ chatDrawerController = createGitHubChatDrawer({ }, }) +prDrawerController = createGitHubPrDrawer({ + featureEnabled: aiAssistantFeatureEnabled, + toggleButton: githubPrToggle, + drawer: githubPrDrawer, + closeButton: githubPrClose, + repositorySelect: githubPrRepoSelect, + baseBranchInput: githubPrBaseBranch, + headBranchInput: githubPrHeadBranch, + componentPathInput: githubPrComponentPath, + stylesPathInput: githubPrStylesPath, + prTitleInput: githubPrTitle, + prBodyInput: githubPrBody, + submitButton: githubPrSubmit, + statusNode: githubPrStatus, + getToken: getCurrentGitHubToken, + getSelectedRepository: getCurrentSelectedRepository, + getWritableRepositories: getCurrentWritableRepositories, + setSelectedRepository: setCurrentSelectedRepository, + getComponentSource: () => getJsxSource(), + getStylesSource: () => getCssSource(), + getDrawerSide: () => { + const layout = getCurrentLayout() + return layout === 'preview-left' ? 'left' : 'right' + }, + confirmBeforeSubmit: options => { + confirmAction(options) + }, + onPullRequestOpened: ({ url }) => { + const message = url + ? `Pull request opened: ${url}` + : 'Pull request opened successfully.' + showAppToast(message) + }, +}) + +prDrawerController.setToken(githubAiContextState.token) +prDrawerController.setSelectedRepository(githubAiContextState.selectedRepository) +prDrawerController.syncRepositories() + const getStyleEditorLanguage = mode => { if (mode === 'less') return 'less' if (mode === 'sass') return 'sass' @@ -985,6 +1087,7 @@ const confirmAction = ({ fallbackConfirmText, onConfirm, }) => { + const toConfirmText = value => (typeof value === 'string' ? value.trim() : '') const supportsModalDialog = clearConfirmDialog instanceof HTMLDialogElement && typeof clearConfirmDialog.showModal === 'function' @@ -1004,7 +1107,25 @@ const confirmAction = ({ clearConfirmTitle.textContent = title } - if (clearConfirmCopy) { + if (clearConfirmCopy instanceof HTMLUListElement) { + const lines = toConfirmText(copy) + .split('\n') + .map(line => line.replace(/^\s*[-*]\s*/, '').trim()) + .filter(Boolean) + + clearConfirmCopy.replaceChildren() + const items = lines.length > 0 ? lines : [toConfirmText(copy)] + + for (const line of items) { + if (!line) { + continue + } + + const listItem = document.createElement('li') + listItem.textContent = line + clearConfirmCopy.append(listItem) + } + } else if (clearConfirmCopy) { clearConfirmCopy.textContent = copy } @@ -1332,10 +1453,15 @@ if (typeof stackedRailMediaQuery.addEventListener === 'function') { } window.addEventListener('beforeunload', () => { + if (appToastDismissTimer) { + clearTimeout(appToastDismissTimer) + appToastDismissTimer = null + } clearComponentLintRecheckTimer() clearStylesLintRecheckTimer() lintDiagnostics.dispose() chatDrawerController.dispose() + prDrawerController.dispose() }) applyAppGridLayout(getInitialAppGridLayout(), { persist: false }) diff --git a/src/index.html b/src/index.html index 9020995..d7fecba 100644 --- a/src/index.html +++ b/src/index.html @@ -115,12 +115,22 @@

- +

+ +
@@ -618,6 +748,8 @@

AI Chat

+ +
@@ -636,7 +768,9 @@

AI Chat

>

Clear source?

-

This action will remove all text from the editor.

+
    +
  • This action will remove all text from the editor.
  • +