Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
72 changes: 72 additions & 0 deletions playwright/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,78 @@ test('BYOT controls render when feature flag is enabled by query param', async (
await expect(page.locator('#github-ai-controls #ai-chat-toggle')).toBeHidden()
})

test('GitHub token info panel reflects missing and present token states', async ({
page,
}) => {
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)

const infoButton = page.locator('#github-token-info')
const infoPanel = page.locator('#github-token-info-panel')
const missingMessage = page.locator('.github-token-info-message--missing-token')
const presentMessage = page.locator('.github-token-info-message--has-token')

await expect(infoButton).toHaveText('?')
await expect(infoButton).toHaveAttribute('data-token-state', 'missing')

await infoButton.click()
await expect(infoPanel).toBeVisible()
await expect(missingMessage).toBeVisible()
await expect(missingMessage).toContainText('Provide a GitHub PAT')
await expect(missingMessage.getByRole('link', { name: 'docs' })).toHaveAttribute(
'href',
'https://github.com/knightedcodemonkey/develop/blob/main/docs/byot.md',
)
await expect(presentMessage).toBeHidden()

await connectByotWithSingleRepo(page)
await expect(infoButton).toHaveText('i')
await expect(infoButton).toHaveAttribute('data-token-state', 'present')

await infoButton.click()
await expect(infoPanel).toBeVisible()
await expect(presentMessage).toBeVisible()
await expect(presentMessage).toContainText(
'Use the trash icon to remove it from storage.',
)
await expect(missingMessage).toBeHidden()
})

test('deleting saved GitHub token requires confirmation modal', async ({ page }) => {
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
await connectByotWithSingleRepo(page)

const dialog = page.locator('#clear-confirm-dialog')
const tokenDelete = page.locator('#github-token-delete')
const tokenAdd = page.locator('#github-token-add')
const tokenInput = page.locator('#github-token-input')

await expect(tokenDelete).toBeVisible()

await tokenDelete.click()
await expect(dialog).toHaveAttribute('open', '')
await expect(page.locator('#clear-confirm-title')).toHaveText(
'Remove saved GitHub token?',
)
await expect(page.locator('#clear-confirm-copy')).toHaveText(
'This action removes the token from browser storage. You can add another token at any time.',
)

await dialog.getByRole('button', { name: 'Cancel' }).click()
await expect(dialog).not.toHaveAttribute('open', '')
await expect(tokenDelete).toBeVisible()
await expect(tokenAdd).toBeHidden()

await tokenDelete.click()
await expect(dialog).toHaveAttribute('open', '')
await dialog.getByRole('button', { name: 'Clear' }).click()
await expect(dialog).not.toHaveAttribute('open', '')

await expect(page.locator('#status')).toHaveText('GitHub token removed')
await expect(tokenAdd).toBeVisible()
await expect(tokenDelete).toBeHidden()
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`)
await connectByotWithSingleRepo(page)
Expand Down
73 changes: 64 additions & 9 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const appGrid = document.querySelector('.app-grid')
const githubAiControls = document.getElementById('github-ai-controls')
const githubTokenInput = document.getElementById('github-token-input')
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')
Expand Down Expand Up @@ -110,6 +111,7 @@ const compactViewportMediaQuery = window.matchMedia('(max-width: 900px)')
const stackedRailMediaQuery = window.matchMedia('(max-width: 1090px)')
let stackedRailViewControlsOpen = false
let compactAiControlsOpen = false
let githubTokenInfoOpen = false

const isStackedRailViewport = () => stackedRailMediaQuery.matches

Expand Down Expand Up @@ -139,13 +141,30 @@ const setStackedRailViewControlsOpen = isOpen => {
viewControlsDrawer.setAttribute('hidden', '')
}

const setGitHubTokenInfoOpen = isOpen => {
if (!(githubTokenInfo instanceof HTMLButtonElement) || !githubTokenInfoPanel) {
return
}

githubTokenInfoOpen = Boolean(isOpen)
githubTokenInfo.setAttribute('aria-expanded', githubTokenInfoOpen ? 'true' : 'false')

if (githubTokenInfoOpen) {
githubTokenInfoPanel.removeAttribute('hidden')
return
}

githubTokenInfoPanel.setAttribute('hidden', '')
}

const setCompactAiControlsOpen = isOpen => {
if (!(aiControlsToggle instanceof HTMLButtonElement) || !githubAiControls) {
return
}

if (!aiAssistantFeatureEnabled) {
compactAiControlsOpen = false
setGitHubTokenInfoOpen(false)
aiControlsToggle.setAttribute('hidden', '')
aiControlsToggle.setAttribute('aria-expanded', 'false')
githubAiControls.removeAttribute('data-compact-open')
Expand All @@ -157,6 +176,7 @@ const setCompactAiControlsOpen = isOpen => {

if (!isCompactViewport()) {
compactAiControlsOpen = false
setGitHubTokenInfoOpen(false)
aiControlsToggle.setAttribute('aria-expanded', 'false')
githubAiControls.removeAttribute('data-compact-open')
githubAiControls.removeAttribute('hidden')
Expand All @@ -166,6 +186,10 @@ const setCompactAiControlsOpen = isOpen => {
compactAiControlsOpen = Boolean(isOpen)
aiControlsToggle.setAttribute('aria-expanded', compactAiControlsOpen ? 'true' : 'false')
githubAiControls.dataset.compactOpen = compactAiControlsOpen ? 'true' : 'false'

if (!compactAiControlsOpen) {
setGitHubTokenInfoOpen(false)
}
}

const getCurrentLayout = () => {
Expand Down Expand Up @@ -508,6 +532,15 @@ const byotControls = createGitHubByotControls({
githubAiContextState.selectedRepository = repository
chatDrawerController.setSelectedRepository(repository)
},
onTokenDeleteRequest: onConfirm => {
confirmAction({
title: 'Remove saved GitHub token?',
copy: 'This action removes the token from browser storage. You can add another token at any time.',
fallbackConfirmText:
'Remove saved GitHub token? This action removes the token from browser storage.',
onConfirm,
})
Comment thread
knightedcodemonkey marked this conversation as resolved.
},
onTokenChange: token => {
githubAiContextState.token = token
syncAiChatTokenVisibility(token)
Expand Down Expand Up @@ -943,17 +976,13 @@ const clearStylesSource = () => {
maybeRender()
}

const confirmClearSource = ({ label, onConfirm }) => {
const confirmAction = ({ title, copy, fallbackConfirmText, onConfirm }) => {
const supportsModalDialog =
clearConfirmDialog instanceof HTMLDialogElement &&
typeof clearConfirmDialog.showModal === 'function'

Comment thread
knightedcodemonkey marked this conversation as resolved.
Outdated
if (!supportsModalDialog) {
if (
window.confirm(
`Clear ${label.toLowerCase()} source? This action will remove all text from the editor.`,
)
) {
if (window.confirm(fallbackConfirmText)) {
onConfirm()
}
return
Expand All @@ -964,18 +993,26 @@ const confirmClearSource = ({ label, onConfirm }) => {
}

if (clearConfirmTitle) {
clearConfirmTitle.textContent = `Clear ${label} source?`
clearConfirmTitle.textContent = title
}

if (clearConfirmCopy) {
clearConfirmCopy.textContent =
'This action will remove all text from the editor. This cannot be undone.'
clearConfirmCopy.textContent = copy
}

pendingClearAction = onConfirm
clearConfirmDialog.showModal()
}

const confirmClearSource = ({ label, onConfirm }) => {
confirmAction({
title: `Clear ${label} source?`,
copy: 'This action will remove all text from the editor. This cannot be undone.',
fallbackConfirmText: `Clear ${label.toLowerCase()} source? This action will remove all text from the editor.`,
onConfirm,
})
}

const copyTextToClipboard = async text => {
if (!clipboardSupported) {
throw new Error('Clipboard API is not available in this browser context.')
Expand Down Expand Up @@ -1186,6 +1223,13 @@ if (aiControlsToggle instanceof HTMLButtonElement) {
})
}

if (githubTokenInfo instanceof HTMLButtonElement && githubTokenInfoPanel) {
githubTokenInfo.addEventListener('click', event => {
event.preventDefault()
setGitHubTokenInfoOpen(!githubTokenInfoOpen)
})
}

document.addEventListener('click', event => {
const clickTarget = event.target
if (!(clickTarget instanceof Node)) {
Expand All @@ -1209,6 +1253,15 @@ document.addEventListener('click', event => {
setCompactAiControlsOpen(false)
}
}

if (githubTokenInfoOpen) {
if (
!githubTokenInfo?.contains(clickTarget) &&
!githubTokenInfoPanel?.contains(clickTarget)
) {
setGitHubTokenInfoOpen(false)
}
}
})

document.addEventListener('keydown', event => {
Expand All @@ -1218,6 +1271,7 @@ document.addEventListener('keydown', event => {

setStackedRailViewControlsOpen(false)
setCompactAiControlsOpen(false)
setGitHubTokenInfoOpen(false)
})

for (const button of editorToolsButtons) {
Expand Down Expand Up @@ -1277,6 +1331,7 @@ applyEditorToolsVisibility()
applyPanelCollapseState()
setStackedRailViewControlsOpen(false)
setCompactAiControlsOpen(false)
setGitHubTokenInfoOpen(false)
syncAiChatTokenVisibility(githubAiContextState.token)

updateRenderButtonVisibility()
Expand Down
26 changes: 22 additions & 4 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,33 @@ <h1>
>
<div class="github-token-control-wrap">
<button
class="hint-icon shadow-hint github-token-info"
class="github-token-info"
id="github-token-info"
type="button"
aria-label="About GitHub token"
aria-describedby="github-token-privacy-note"
data-tooltip="This token is stored only in your browser and is sent only to GitHub APIs you invoke."
aria-label="About GitHub token features and privacy"
aria-expanded="false"
aria-controls="github-token-info-panel"
>
i
</button>
<div class="github-token-info-panel" id="github-token-info-panel" hidden>
<p
class="github-token-info-message github-token-info-message--missing-token"
>
Provide a GitHub PAT to open pull requests against your repos or chat with
GitHub models. Read more about it in the
<a
href="https://github.com/knightedcodemonkey/develop/blob/main/docs/byot.md"
target="_blank"
rel="noreferrer noopener"
>docs</a
>.
</p>
<p class="github-token-info-message github-token-info-message--has-token">
This token is stored only in your browser and is sent only to GitHub APIs
you invoke. Use the trash icon to remove it from storage.
</p>
</div>
<label class="sr-only" for="github-token-input">GitHub token</label>
<input
class="github-token-input"
Expand Down
28 changes: 27 additions & 1 deletion src/modules/github-byot-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const createGitHubByotControls = ({
repoSelect,
repoWrap,
onRepositoryChange,
onTokenDeleteRequest,
onTokenChange,
setStatus,
}) => {
Expand Down Expand Up @@ -114,6 +115,22 @@ export const createGitHubByotControls = ({
const hasProvidedToken =
typeof savedToken === 'string' && savedToken.trim().length > 0

if (tokenInfoButton instanceof HTMLButtonElement) {
tokenInfoButton.dataset.tokenState = hasProvidedToken ? 'present' : 'missing'
tokenInfoButton.textContent = hasProvidedToken ? 'i' : '?'
tokenInfoButton.setAttribute(
'aria-label',
hasProvidedToken
? 'About GitHub token privacy'
: 'About GitHub token features and privacy',
)

const tokenControlWrap = tokenInfoButton.closest('.github-token-control-wrap')
if (tokenControlWrap instanceof HTMLElement) {
tokenControlWrap.dataset.tokenState = hasProvidedToken ? 'present' : 'missing'
}
}

if (tokenAddButton instanceof HTMLButtonElement) {
tokenAddButton.hidden = hasProvidedToken
}
Expand Down Expand Up @@ -413,7 +430,7 @@ export const createGitHubByotControls = ({
void persistAndLoadToken(tokenInput.value)
})

tokenDeleteButton?.addEventListener('click', () => {
const removeSavedToken = () => {
abortInFlightRepoRequest()
clearAddButtonResetTimer()
setTokenAddButtonState('idle')
Expand All @@ -426,6 +443,15 @@ export const createGitHubByotControls = ({
onRepositoryChange?.(null)
syncSavedTokenUi()
setStatus('GitHub token removed', 'neutral')
}

tokenDeleteButton?.addEventListener('click', () => {
if (typeof onTokenDeleteRequest === 'function') {
onTokenDeleteRequest(removeSavedToken)
return
}

removeSavedToken()
})

tokenInfoButton?.setAttribute('aria-expanded', 'false')
Expand Down
Loading
Loading