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 = []