Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 2 additions & 10 deletions docs/byot.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,14 @@ 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

- Your token is stored only in your browser `localStorage`.
- 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.
Expand Down Expand Up @@ -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

Expand Down
36 changes: 17 additions & 19 deletions playwright/github-byot-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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', {
Expand All @@ -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',
Expand Down Expand Up @@ -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', {
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 = () => <button>Before</button>')
await setStylesEditorSource(page, '.button { color: red; }')
Expand Down Expand Up @@ -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 = () => <button>Before</button>')
await setStylesEditorSource(page, '.button { color: red; }')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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' })
Expand Down
40 changes: 20 additions & 20 deletions playwright/github-pr-drawer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 1 addition & 9 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -760,7 +753,6 @@ const prEditorSyncController = createGitHubPrEditorSyncController({
})

chatDrawerController = createGitHubChatDrawer({
featureEnabled: aiAssistantFeatureEnabled,
toggleButton: aiChatToggle,
drawer: aiChatDrawer,
closeButton: aiChatClose,
Expand Down
Loading
Loading