diff --git a/README.md b/README.md index bde818c..81cad31 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,6 @@ browser acts as the runtime host for render, lint, and typecheck flows. - GitHub PAT setup and usage: [docs/byot.md](docs/byot.md) -AI chat features remain opt-in behind -`?feature-ai=true`. - ## Fine-Grained PAT Quick Setup For PR/BYOT and AI chat flows, use a fine-grained GitHub PAT and follow the diff --git a/docs/byot.md b/docs/byot.md index ae17249..eba1bc9 100644 --- a/docs/byot.md +++ b/docs/byot.md @@ -11,7 +11,7 @@ BYOT controls are available by default. The token is used to: - let you choose which repository to work with - use PR context features (Open PR / Push Commit flows) -When AI chat is enabled, the same token is also used for GitHub Models requests. +The same token is also used for GitHub Models requests in AI chat flows. ## Privacy and storage behavior @@ -19,14 +19,6 @@ When AI chat is enabled, the same token is also used for GitHub Models requests. - The token is never sent to any service except the GitHub endpoints required by the feature. - You can remove it at any time using the delete button in the BYOT controls. -## Enable AI chat features - -BYOT/PR controls do not require a feature flag. To enable AI chat features, use one -of these options: - -1. Add `?feature-ai=true` to the app URL. -2. Set `localStorage` key `knighted:develop:feature:ai-assistant` to `true`. - ## Create a fine-grained PAT Create a fine-grained PAT in GitHub settings and grant the permissions below. @@ -63,7 +55,7 @@ Use either of these scopes depending on your needs: 3. Paste token into the BYOT input and click add. 4. Verify repository list loads. 5. Select your target repository. -6. Optional: enable AI chat with `?feature-ai=true`. +6. Use AI chat as needed after connecting your token. ## Screenshots diff --git a/playwright/github-byot-ai.spec.ts b/playwright/github-byot-ai.spec.ts index 6277fd0..ec00e48 100644 --- a/playwright/github-byot-ai.spec.ts +++ b/playwright/github-byot-ai.spec.ts @@ -12,7 +12,7 @@ import { waitForAppReady, } from './helpers/app-test-helpers.js' -test('PR/BYOT controls are visible without feature flag, but chat stays hidden', async ({ +test('PR/BYOT controls are visible and chat stays hidden until token connect', async ({ page, }) => { await waitForAppReady(page) @@ -32,18 +32,16 @@ test('PR/BYOT controls are visible without feature flag, but chat stays hidden', await expect(prToggle).toBeHidden() }) -test('chat remains hidden without feature flag after token connect', async ({ page }) => { +test('chat becomes available after token connect', async ({ page }) => { await waitForAppReady(page) await connectByotWithSingleRepo(page) await expect(page.getByRole('button', { name: 'Open pull request' })).toBeVisible() - await expect(page.getByRole('button', { name: 'Chat' })).toBeHidden() + await expect(page.getByRole('button', { name: 'Chat' })).toBeVisible() }) -test('BYOT controls render when feature flag is enabled by query param', async ({ - page, -}) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) +test('BYOT controls render with default app entry', async ({ page }) => { + await waitForAppReady(page, appEntryPath) const byotControls = page.getByRole('group', { name: 'GitHub controls' }) const prToggle = page.getByRole('button', { @@ -62,7 +60,7 @@ test('BYOT controls render when feature flag is enabled by query param', async ( test('GitHub token info panel reflects missing and present token states', async ({ page, }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) const infoButtonMissing = page.getByRole('button', { name: 'About GitHub token features and privacy', @@ -107,7 +105,7 @@ test('GitHub token info panel reflects missing and present token states', async }) test('deleting saved GitHub token requires confirmation modal', async ({ page }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) const dialog = page.getByRole('dialog', { @@ -154,8 +152,8 @@ test('deleting saved GitHub token requires confirmation modal', async ({ page }) await expect(tokenInput).toHaveValue('') }) -test('AI chat drawer opens and closes when feature flag is enabled', async ({ page }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) +test('AI chat drawer opens and closes', async ({ page }) => { + await waitForAppReady(page, appEntryPath) await connectByotWithSingleRepo(page) const chatToggle = page.getByRole('button', { name: 'Chat', exact: true }) @@ -193,7 +191,7 @@ test('AI chat prefers streaming responses when available', async ({ page }) => { }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureAiChatDrawerOpen(page) @@ -255,7 +253,7 @@ test('AI chat can disable editor context payload via checkbox', async ({ page }) }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureAiChatDrawerOpen(page) @@ -350,7 +348,7 @@ test('AI chat proposals can be confirmed, applied, and undone for component and }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await setComponentEditorSource(page, 'const App = () => ') await setStylesEditorSource(page, '.button { color: red; }') @@ -462,7 +460,7 @@ test('AI chat shows a single apply action when both editor proposals are availab }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await setComponentEditorSource(page, 'const App = () => ') await setStylesEditorSource(page, '.button { color: red; }') @@ -560,7 +558,7 @@ test('AI chat streaming text still updates while latest undo actions are visible }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await setStylesEditorSource(page, '.button { color: red; }') await ensureAiChatDrawerOpen(page) @@ -626,7 +624,7 @@ test('AI chat falls back to non-streaming response when streaming fails', async }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureAiChatDrawerOpen(page) @@ -678,7 +676,7 @@ test('clearing chat removes previous conversation context from new request', asy }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureAiChatDrawerOpen(page) @@ -738,7 +736,7 @@ test('BYOT remembers selected repository across reloads', async ({ page }) => { 'knightedcodemonkey/css': ['main', 'release/1.x'], }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page .getByRole('textbox', { name: 'GitHub token' }) diff --git a/playwright/github-pr-drawer.spec.ts b/playwright/github-pr-drawer.spec.ts index 4712458..7d550bc 100644 --- a/playwright/github-pr-drawer.spec.ts +++ b/playwright/github-pr-drawer.spec.ts @@ -189,7 +189,7 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({ }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureOpenPrDrawerOpen(page) @@ -254,7 +254,7 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({ test('Open PR drawer starts with empty title/description and short default head', async ({ page, }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureOpenPrDrawerOpen(page) @@ -309,7 +309,7 @@ test('Open PR drawer base dropdown updates from mocked repo branches', async ({ }) }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page .getByRole('textbox', { name: 'GitHub token' }) @@ -377,7 +377,7 @@ test('Open PR drawer keeps a single active PR context in localStorage', async ({ 'knightedcodemonkey/css': ['stable', 'release/1.x'], }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page .getByRole('textbox', { name: 'GitHub token' }) @@ -452,7 +452,7 @@ test('Open PR drawer does not prune saved PR context on repo switch before save' 'knightedcodemonkey/css': ['stable', 'release/1.x'], }) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page .getByRole('textbox', { name: 'GitHub token' }) @@ -571,7 +571,7 @@ test('Active PR context updates controls and can be closed from AI controls', as }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem( @@ -661,7 +661,7 @@ test('Active PR context is disabled on load when pull request is closed', async }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem( @@ -759,7 +759,7 @@ test('Active PR context rehydrates after token remove and re-add', async ({ page }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem('knighted:develop:github-repository', 'knightedcodemonkey/css') @@ -870,7 +870,7 @@ test('Active PR context deactivates after token remove and re-add when PR is clo }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem('knighted:develop:github-repository', 'knightedcodemonkey/css') @@ -985,7 +985,7 @@ test('Active PR context recovers when saved head branch is missing but PR metada }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem( @@ -1130,7 +1130,7 @@ test('Active PR context uses Push commit flow without creating a new pull reques }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem( @@ -1327,7 +1327,7 @@ test('Reloaded active PR context from URL metadata keeps Push mode and status re }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem( @@ -1474,7 +1474,7 @@ test('Reloaded active PR context syncs editor content from GitHub branch and res }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem( @@ -1559,7 +1559,7 @@ test('Reloaded active PR context falls back to css style mode for unsupported va }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await page.evaluate(() => { localStorage.setItem( @@ -1585,7 +1585,7 @@ test('Reloaded active PR context falls back to css style mode for unsupported va }) test('Open PR drawer validates unsafe filepaths', async ({ page }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureOpenPrDrawerOpen(page) @@ -1605,7 +1605,7 @@ test('Open PR drawer validates unsafe filepaths', async ({ page }) => { test('Open PR drawer allows dotted file segments that are not traversal', async ({ page, }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureOpenPrDrawerOpen(page) @@ -1626,7 +1626,7 @@ test('Open PR drawer allows dotted file segments that are not traversal', async }) test('Open PR drawer rejects trailing slash file paths', async ({ page }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureOpenPrDrawerOpen(page) @@ -1645,7 +1645,7 @@ test('Open PR drawer rejects trailing slash file paths', async ({ page }) => { test('Open PR drawer include App wrapper checkbox defaults off and resets on reopen', async ({ page, }) => { - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await ensureOpenPrDrawerOpen(page) @@ -1755,7 +1755,7 @@ test('Open PR drawer strips App wrapper from committed component source by defau }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) const componentSource = [ @@ -1880,7 +1880,7 @@ test('Open PR drawer includes App wrapper in committed source when toggled on', }, ) - await waitForAppReady(page, `${appEntryPath}?feature-ai=true`) + await waitForAppReady(page, `${appEntryPath}`) await connectByotWithSingleRepo(page) await setComponentEditorSource( diff --git a/src/app.js b/src/app.js index 699891d..ed193d6 100644 --- a/src/app.js +++ b/src/app.js @@ -7,7 +7,6 @@ import { import { createCodeMirrorEditor } from './modules/editor-codemirror.js' import { defaultCss, defaultJsx, defaultReactJsx } from './modules/defaults.js' import { createDiagnosticsUiController } from './modules/diagnostics-ui.js' -import { isAiAssistantFeatureEnabled } from './modules/feature-flags.js' import { createGitHubChatDrawer } from './modules/github-chat-drawer/drawer.js' import { createGitHubByotControls } from './modules/github-byot-controls.js' import { @@ -120,7 +119,6 @@ let suppressEditorChangeSideEffects = false let hasAppliedReactModeDefault = false let appToastDismissTimer = null const clipboardSupported = Boolean(navigator.clipboard?.writeText) -const aiAssistantFeatureEnabled = isAiAssistantFeatureEnabled() const githubPrOpenIcon = { viewBox: '0 0 16 16', path: 'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z', @@ -652,12 +650,7 @@ const syncAiChatTokenVisibility = token => { const hasToken = typeof token === 'string' && token.trim().length > 0 if (hasToken) { - if (aiAssistantFeatureEnabled) { - aiChatToggle?.removeAttribute('hidden') - } else { - aiChatToggle?.setAttribute('hidden', '') - aiChatToggle?.setAttribute('aria-expanded', 'false') - } + aiChatToggle?.removeAttribute('hidden') githubPrToggle?.removeAttribute('hidden') @@ -760,7 +753,6 @@ const prEditorSyncController = createGitHubPrEditorSyncController({ }) chatDrawerController = createGitHubChatDrawer({ - featureEnabled: aiAssistantFeatureEnabled, toggleButton: aiChatToggle, drawer: aiChatDrawer, closeButton: aiChatClose, diff --git a/src/modules/feature-flags.js b/src/modules/feature-flags.js deleted file mode 100644 index 6cdda16..0000000 --- a/src/modules/feature-flags.js +++ /dev/null @@ -1,64 +0,0 @@ -const aiFeatureStorageKey = 'knighted:develop:feature:ai-assistant' -const aiFeatureQueryKey = 'feature-ai' - -const parseBooleanLikeValue = value => { - if (typeof value !== 'string') { - return null - } - - const normalized = value.trim().toLowerCase() - - if (['1', 'true', 'on', 'yes', 'enabled'].includes(normalized)) { - return true - } - - if (['0', 'false', 'off', 'no', 'disabled'].includes(normalized)) { - return false - } - - return null -} - -const readBooleanFromLocalStorage = key => { - try { - const storedValue = localStorage.getItem(key) - return parseBooleanLikeValue(storedValue) - } catch { - return null - } -} - -const readBooleanFromQueryParam = key => { - if (typeof window === 'undefined') { - return null - } - - const params = new URLSearchParams(window.location.search) - if (!params.has(key)) { - return null - } - - return parseBooleanLikeValue(params.get(key)) -} - -export const isAiAssistantFeatureEnabled = () => { - const queryValue = readBooleanFromQueryParam(aiFeatureQueryKey) - if (queryValue !== null) { - return queryValue - } - - const localStorageValue = readBooleanFromLocalStorage(aiFeatureStorageKey) - if (localStorageValue !== null) { - return localStorageValue - } - - return false -} - -export const setAiAssistantFeatureEnabled = isEnabled => { - try { - localStorage.setItem(aiFeatureStorageKey, isEnabled ? 'true' : 'false') - } catch { - /* noop */ - } -} diff --git a/src/modules/github-chat-drawer/drawer.js b/src/modules/github-chat-drawer/drawer.js index f4b92dd..f70b579 100644 --- a/src/modules/github-chat-drawer/drawer.js +++ b/src/modules/github-chat-drawer/drawer.js @@ -42,7 +42,6 @@ const createMessageLabelIconTemplate = role => { } export const createGitHubChatDrawer = ({ - featureEnabled, toggleButton, drawer, closeButton, @@ -65,19 +64,6 @@ export const createGitHubChatDrawer = ({ getStyleMode, getDrawerSide, }) => { - if (!featureEnabled) { - toggleButton?.setAttribute('hidden', '') - drawer?.setAttribute('hidden', '') - - return { - setOpen: () => {}, - isOpen: () => false, - setSelectedRepository: () => {}, - setToken: () => {}, - dispose: () => {}, - } - } - let open = false let pendingAbortController = null const messages = []