From 1ee0fa71e54cc90f33349688e1f4678fac219282 Mon Sep 17 00:00:00 2001 From: KCM Date: Mon, 23 Mar 2026 21:16:09 -0500 Subject: [PATCH 1/3] refactor: named async func instead of iife. --- src/modules/type-diagnostics.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/type-diagnostics.js b/src/modules/type-diagnostics.js index 33a08dd..c97f6ec 100644 --- a/src/modules/type-diagnostics.js +++ b/src/modules/type-diagnostics.js @@ -483,7 +483,7 @@ export const createTypeDiagnosticsController = ({ return reactTypeLoadPromise } - reactTypeLoadPromise = (async () => { + const loadReactTypeFiles = async () => { const files = new Map() const packageEntryByName = new Map() const packageManifestByName = new Map() @@ -631,7 +631,9 @@ export const createTypeDiagnosticsController = ({ files, packageEntries: packageEntryByName, } - })() + } + + reactTypeLoadPromise = loadReactTypeFiles() try { return await reactTypeLoadPromise From de3da84573c07755480651d37f5ec20676e795a6 Mon Sep 17 00:00:00 2001 From: KCM Date: Tue, 24 Mar 2026 09:11:41 -0500 Subject: [PATCH 2/3] refactor: store only one pr. --- playwright/app.spec.ts | 73 +++++++++++++++++++++++++++++++++ src/modules/github-pr-drawer.js | 40 ++++++++++++++++-- src/styles/ai-controls.css | 4 +- src/styles/dialogs-overlays.css | 7 ++-- 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index bdd38b5..f933673 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -830,6 +830,79 @@ test('Open PR drawer base dropdown updates from mocked repo branches', async ({ ).toBe(true) }) +test('Open PR drawer keeps a single active PR context in localStorage', async ({ + page, +}) => { + await page.route('https://api.github.com/user/repos**', async route => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 2, + owner: { login: 'knightedcodemonkey' }, + name: 'develop', + full_name: 'knightedcodemonkey/develop', + default_branch: 'main', + permissions: { push: true }, + }, + { + id: 1, + owner: { login: 'knightedcodemonkey' }, + name: 'css', + full_name: 'knightedcodemonkey/css', + default_branch: 'stable', + permissions: { push: true }, + }, + ]), + }) + }) + + await mockRepositoryBranches(page, { + 'knightedcodemonkey/develop': ['main', 'develop-next'], + 'knightedcodemonkey/css': ['stable', 'release/1.x'], + }) + + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + + await page.locator('#github-token-input').fill('github_pat_fake_1234567890') + await page.locator('#github-token-add').click() + await ensureOpenPrDrawerOpen(page) + + const repoSelect = page.locator('#github-pr-repo-select') + const componentPath = page.locator('#github-pr-component-path') + + await repoSelect.selectOption('knightedcodemonkey/develop') + await componentPath.fill('examples/develop/App.tsx') + await componentPath.blur() + + await repoSelect.selectOption('knightedcodemonkey/css') + await componentPath.fill('examples/css/App.tsx') + await componentPath.blur() + + const activeContext = await page.evaluate(() => { + const storagePrefix = 'knighted:develop:github-pr-config:' + const keys = Object.keys(localStorage).filter(key => key.startsWith(storagePrefix)) + const key = keys[0] ?? null + const raw = key ? localStorage.getItem(key) : null + + let parsed = null + try { + parsed = raw ? JSON.parse(raw) : null + } catch { + parsed = null + } + + return { keys, key, parsed } + }) + + expect(activeContext.keys).toHaveLength(1) + expect(activeContext.key).toBe( + 'knighted:develop:github-pr-config:knightedcodemonkey/css', + ) + expect(activeContext.parsed?.componentFilePath).toBe('examples/css/App.tsx') +}) + test('Open PR drawer validates unsafe filepaths', async ({ page }) => { await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) await connectByotWithSingleRepo(page) diff --git a/src/modules/github-pr-drawer.js b/src/modules/github-pr-drawer.js index 13e51d0..1fcca8e 100644 --- a/src/modules/github-pr-drawer.js +++ b/src/modules/github-pr-drawer.js @@ -7,11 +7,42 @@ const defaultPrConfig = { stylesFilePath: 'src/styles/app.css', } +const pruneRepositoryPrConfigs = repositoryFullName => { + if (typeof repositoryFullName !== 'string' || !repositoryFullName.trim()) { + return + } + + const activeStorageKey = `${prConfigStoragePrefix}${repositoryFullName}` + + try { + const keysToRemove = [] + + for (let index = 0; index < localStorage.length; index += 1) { + const key = localStorage.key(index) + if (!key || !key.startsWith(prConfigStoragePrefix)) { + continue + } + + if (key !== activeStorageKey) { + keysToRemove.push(key) + } + } + + for (const key of keysToRemove) { + localStorage.removeItem(key) + } + } catch { + /* noop */ + } +} + const readRepositoryPrConfig = repositoryFullName => { if (typeof repositoryFullName !== 'string' || !repositoryFullName.trim()) { return {} } + pruneRepositoryPrConfigs(repositoryFullName) + try { const value = localStorage.getItem(`${prConfigStoragePrefix}${repositoryFullName}`) if (!value) { @@ -31,10 +62,11 @@ const saveRepositoryPrConfig = ({ repositoryFullName, config }) => { } try { - localStorage.setItem( - `${prConfigStoragePrefix}${repositoryFullName}`, - JSON.stringify(config), - ) + const activeStorageKey = `${prConfigStoragePrefix}${repositoryFullName}` + + localStorage.setItem(activeStorageKey, JSON.stringify(config)) + + pruneRepositoryPrConfigs(repositoryFullName) } catch { /* noop */ } diff --git a/src/styles/ai-controls.css b/src/styles/ai-controls.css index fa9f45b..03e0486 100644 --- a/src/styles/ai-controls.css +++ b/src/styles/ai-controls.css @@ -100,8 +100,8 @@ opacity: 0.9; background: transparent; padding: 0; - width: 24px; - height: 24px; + width: 20px; + height: 20px; font-size: 0.74rem; transition: border-color 140ms ease, diff --git a/src/styles/dialogs-overlays.css b/src/styles/dialogs-overlays.css index a12479a..67a293b 100644 --- a/src/styles/dialogs-overlays.css +++ b/src/styles/dialogs-overlays.css @@ -102,7 +102,8 @@ .app-toast { position: fixed; - top: 76px; + top: auto; + bottom: 12px; right: 24px; max-width: min(560px, calc(100vw - 48px)); border-radius: 12px; @@ -115,7 +116,7 @@ line-height: 1.35; z-index: 140; opacity: 0; - transform: translateY(-6px); + transform: translateY(6px); transition: opacity 170ms ease, transform 170ms ease; @@ -129,8 +130,6 @@ @media (max-width: 900px) { .app-toast { - top: auto; - bottom: 12px; left: 12px; right: 12px; max-width: none; From da6d56868fc38b603a471ea97b5ec7b8c0e0ef7d Mon Sep 17 00:00:00 2001 From: KCM Date: Tue, 24 Mar 2026 09:47:53 -0500 Subject: [PATCH 3/3] refactor: only prune on save. --- playwright/app.spec.ts | 75 +++++++++++++++++++++++++++++++++ src/modules/github-pr-drawer.js | 2 - 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index f933673..47ffcaf 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -903,6 +903,81 @@ test('Open PR drawer keeps a single active PR context in localStorage', async ({ expect(activeContext.parsed?.componentFilePath).toBe('examples/css/App.tsx') }) +test('Open PR drawer does not prune saved PR context on repo switch before save', async ({ + page, +}) => { + await page.route('https://api.github.com/user/repos**', async route => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 2, + owner: { login: 'knightedcodemonkey' }, + name: 'develop', + full_name: 'knightedcodemonkey/develop', + default_branch: 'main', + permissions: { push: true }, + }, + { + id: 1, + owner: { login: 'knightedcodemonkey' }, + name: 'css', + full_name: 'knightedcodemonkey/css', + default_branch: 'stable', + permissions: { push: true }, + }, + ]), + }) + }) + + await mockRepositoryBranches(page, { + 'knightedcodemonkey/develop': ['main', 'develop-next'], + 'knightedcodemonkey/css': ['stable', 'release/1.x'], + }) + + await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + + await page.locator('#github-token-input').fill('github_pat_fake_1234567890') + await page.locator('#github-token-add').click() + await ensureOpenPrDrawerOpen(page) + + const repoSelect = page.locator('#github-pr-repo-select') + const componentPath = page.locator('#github-pr-component-path') + + await repoSelect.selectOption('knightedcodemonkey/develop') + await componentPath.fill('examples/develop/App.tsx') + await componentPath.blur() + + await repoSelect.selectOption('knightedcodemonkey/css') + + const contexts = await page.evaluate(() => { + const storagePrefix = 'knighted:develop:github-pr-config:' + const keys = Object.keys(localStorage) + .filter(key => key.startsWith(storagePrefix)) + .sort((left, right) => left.localeCompare(right)) + + return keys.map(key => { + const raw = localStorage.getItem(key) + let parsed = null + + try { + parsed = raw ? JSON.parse(raw) : null + } catch { + parsed = null + } + + return { key, parsed } + }) + }) + + expect(contexts).toHaveLength(1) + expect(contexts[0]?.key).toBe( + 'knighted:develop:github-pr-config:knightedcodemonkey/develop', + ) + expect(contexts[0]?.parsed?.componentFilePath).toBe('examples/develop/App.tsx') +}) + test('Open PR drawer validates unsafe filepaths', async ({ page }) => { await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) await connectByotWithSingleRepo(page) diff --git a/src/modules/github-pr-drawer.js b/src/modules/github-pr-drawer.js index 1fcca8e..c8729f1 100644 --- a/src/modules/github-pr-drawer.js +++ b/src/modules/github-pr-drawer.js @@ -41,8 +41,6 @@ const readRepositoryPrConfig = repositoryFullName => { return {} } - pruneRepositoryPrConfigs(repositoryFullName) - try { const value = localStorage.getItem(`${prConfigStoragePrefix}${repositoryFullName}`) if (!value) {