Skip to content

Commit 1bc9ef1

Browse files
feat: disconnect from pr context. (#61)
1 parent e274616 commit 1bc9ef1

File tree

5 files changed

+373
-30
lines changed

5 files changed

+373
-30
lines changed

playwright/github-pr-drawer.spec.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,179 @@ test('Open PR drawer does not prune saved PR context on repo switch before save'
496496
expect(contexts[0]?.parsed?.componentFilePath).toBe('examples/develop/App.tsx')
497497
})
498498

499+
test('Active PR context disconnect uses local-only confirmation flow', async ({
500+
page,
501+
}) => {
502+
let closePullRequestRequestCount = 0
503+
504+
await page.route('https://api.github.com/user/repos**', async route => {
505+
await route.fulfill({
506+
status: 200,
507+
contentType: 'application/json',
508+
body: JSON.stringify([
509+
{
510+
id: 11,
511+
owner: { login: 'knightedcodemonkey' },
512+
name: 'develop',
513+
full_name: 'knightedcodemonkey/develop',
514+
default_branch: 'main',
515+
permissions: { push: true },
516+
},
517+
]),
518+
})
519+
})
520+
521+
await mockRepositoryBranches(page, {
522+
'knightedcodemonkey/develop': ['main', 'release'],
523+
})
524+
525+
await page.route(
526+
'https://api.github.com/repos/knightedcodemonkey/develop/pulls/2',
527+
async route => {
528+
if (route.request().method() === 'PATCH') {
529+
closePullRequestRequestCount += 1
530+
await route.fulfill({
531+
status: 200,
532+
contentType: 'application/json',
533+
body: JSON.stringify({
534+
number: 2,
535+
state: 'closed',
536+
title: 'Existing PR context from storage',
537+
html_url: 'https://github.com/knightedcodemonkey/develop/pull/2',
538+
head: { ref: 'develop/open-pr-test' },
539+
base: { ref: 'main' },
540+
}),
541+
})
542+
return
543+
}
544+
545+
await route.fulfill({
546+
status: 200,
547+
contentType: 'application/json',
548+
body: JSON.stringify({
549+
number: 2,
550+
state: 'open',
551+
title: 'Existing PR context from storage',
552+
html_url: 'https://github.com/knightedcodemonkey/develop/pull/2',
553+
head: { ref: 'develop/open-pr-test' },
554+
base: { ref: 'main' },
555+
}),
556+
})
557+
},
558+
)
559+
560+
await page.route(
561+
'https://api.github.com/repos/knightedcodemonkey/develop/git/ref/**',
562+
async route => {
563+
await route.fulfill({
564+
status: 200,
565+
contentType: 'application/json',
566+
body: JSON.stringify({
567+
ref: 'refs/heads/develop/open-pr-test',
568+
object: { type: 'commit', sha: 'existing-head-sha' },
569+
}),
570+
})
571+
},
572+
)
573+
574+
await waitForAppReady(page, `${appEntryPath}`)
575+
576+
await page.evaluate(() => {
577+
localStorage.setItem(
578+
'knighted:develop:github-pr-config:knightedcodemonkey/develop',
579+
JSON.stringify({
580+
componentFilePath: 'examples/component/App.tsx',
581+
stylesFilePath: 'examples/styles/app.css',
582+
renderMode: 'react',
583+
baseBranch: 'main',
584+
headBranch: 'develop/open-pr-test',
585+
prTitle: 'Existing PR context from storage',
586+
prBody: 'Saved body',
587+
isActivePr: true,
588+
pullRequestNumber: 2,
589+
pullRequestUrl: 'https://github.com/knightedcodemonkey/develop/pull/2',
590+
}),
591+
)
592+
})
593+
594+
await connectByotWithSingleRepo(page)
595+
596+
await expect(
597+
page.getByRole('button', { name: 'Disconnect active pull request context' }),
598+
).toBeVisible()
599+
600+
await page
601+
.getByRole('button', { name: 'Disconnect active pull request context' })
602+
.click()
603+
604+
const dialog = page.getByRole('dialog')
605+
await expect(dialog).toBeVisible()
606+
await expect(dialog).toContainText('Disconnect PR context?')
607+
await expect(dialog).toContainText(
608+
'This will disconnect the active pull request context in this app only.',
609+
)
610+
await expect(dialog).toContainText('Your pull request will stay open on GitHub.')
611+
await expect(dialog).toContainText(
612+
'Your GitHub token and selected repository will stay connected.',
613+
)
614+
615+
await dialog.getByRole('button', { name: 'Cancel' }).click()
616+
617+
await expect(
618+
page.getByRole('button', { name: 'Push commit to active pull request branch' }),
619+
).toBeVisible()
620+
621+
const savedActiveStateAfterCancel = await page.evaluate(() => {
622+
const raw = localStorage.getItem(
623+
'knighted:develop:github-pr-config:knightedcodemonkey/develop',
624+
)
625+
626+
if (!raw) {
627+
return null
628+
}
629+
630+
try {
631+
const parsed = JSON.parse(raw)
632+
return parsed?.isActivePr === true
633+
} catch {
634+
return null
635+
}
636+
})
637+
638+
expect(savedActiveStateAfterCancel).toBe(true)
639+
640+
await page
641+
.getByRole('button', { name: 'Disconnect active pull request context' })
642+
.click()
643+
await dialog.getByRole('button', { name: 'Disconnect' }).click()
644+
645+
await expect(page.getByRole('button', { name: 'Open pull request' })).toBeVisible()
646+
await expect(
647+
page.getByRole('button', { name: 'Disconnect active pull request context' }),
648+
).toBeHidden()
649+
650+
const savedContextAfterDisconnect = await page.evaluate(() => {
651+
const raw = localStorage.getItem(
652+
'knighted:develop:github-pr-config:knightedcodemonkey/develop',
653+
)
654+
655+
if (!raw) {
656+
return null
657+
}
658+
659+
try {
660+
return JSON.parse(raw)
661+
} catch {
662+
return null
663+
}
664+
})
665+
666+
expect(savedContextAfterDisconnect).not.toBeNull()
667+
expect(savedContextAfterDisconnect?.isActivePr).toBe(false)
668+
expect(savedContextAfterDisconnect?.pullRequestNumber).toBe(2)
669+
expect(closePullRequestRequestCount).toBe(0)
670+
})
671+
499672
test('Active PR context updates controls and can be closed from AI controls', async ({
500673
page,
501674
}) => {

src/app.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const githubPrToggleLabel = document.getElementById('github-pr-toggle-label')
4747
const githubPrToggleIcon = document.getElementById('github-pr-toggle-icon')
4848
const githubPrToggleIconPath = document.getElementById('github-pr-toggle-icon-path')
4949
const githubPrContextClose = document.getElementById('github-pr-context-close')
50+
const githubPrContextDisconnect = document.getElementById('github-pr-context-disconnect')
5051
const githubPrDrawer = document.getElementById('github-pr-drawer')
5152
const openPrTitle = document.getElementById('open-pr-title')
5253
const githubPrClose = document.getElementById('github-pr-close')
@@ -634,16 +635,14 @@ const syncActivePrContextUi = activeContext => {
634635
setGitHubPrToggleVisual(hasActiveContext ? 'push-commit' : 'open-pr')
635636
syncEditorPrContextIndicators(shouldShowEditorSyncIndicators)
636637

637-
if (!(githubPrContextClose instanceof HTMLButtonElement)) {
638-
return
639-
}
640-
641638
if (!hasActiveContext) {
642-
githubPrContextClose.setAttribute('hidden', '')
639+
githubPrContextClose?.setAttribute('hidden', '')
640+
githubPrContextDisconnect?.setAttribute('hidden', '')
643641
return
644642
}
645643

646-
githubPrContextClose.removeAttribute('hidden')
644+
githubPrContextClose?.removeAttribute('hidden')
645+
githubPrContextDisconnect?.removeAttribute('hidden')
647646
}
648647

649648
const syncAiChatTokenVisibility = token => {
@@ -656,8 +655,10 @@ const syncAiChatTokenVisibility = token => {
656655

657656
if (githubAiContextState.activePrContext) {
658657
githubPrContextClose?.removeAttribute('hidden')
658+
githubPrContextDisconnect?.removeAttribute('hidden')
659659
} else {
660660
githubPrContextClose?.setAttribute('hidden', '')
661+
githubPrContextDisconnect?.setAttribute('hidden', '')
661662
}
662663
return
663664
}
@@ -672,6 +673,7 @@ const syncAiChatTokenVisibility = token => {
672673
githubPrToggle?.setAttribute('hidden', '')
673674
githubPrToggle?.setAttribute('aria-expanded', 'false')
674675
githubPrContextClose?.setAttribute('hidden', '')
676+
githubPrContextDisconnect?.setAttribute('hidden', '')
675677
chatDrawerController.setOpen(false)
676678
prDrawerController.setOpen(false)
677679
}
@@ -919,6 +921,31 @@ githubPrContextClose?.addEventListener('click', () => {
919921
})
920922
})
921923

924+
githubPrContextDisconnect?.addEventListener('click', () => {
925+
if (!githubAiContextState.activePrContext) {
926+
return
927+
}
928+
929+
const activePrReference = formatActivePrReference(githubAiContextState.activePrContext)
930+
const referenceLine = activePrReference ? `PR: ${activePrReference}\n` : ''
931+
932+
confirmAction({
933+
title: 'Disconnect PR context?',
934+
copy: `${referenceLine}This will disconnect the active pull request context in this app only.\nYour pull request will stay open on GitHub.\nYour GitHub token and selected repository will stay connected.`,
935+
confirmButtonText: 'Disconnect',
936+
onConfirm: () => {
937+
const result = prDrawerController.disconnectActivePrContext()
938+
const reference = result?.reference
939+
setStatus(
940+
reference
941+
? `Disconnected PR context (${reference}). Pull request remains open on GitHub.`
942+
: 'Disconnected PR context. Pull request remains open on GitHub.',
943+
'neutral',
944+
)
945+
},
946+
})
947+
})
948+
922949
const getStyleEditorLanguage = mode => {
923950
if (mode === 'less') return 'less'
924951
if (mode === 'sass') return 'sass'

src/index.html

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,30 @@ <h1>
177177
<span class="github-pr-context-close__label">Close</span>
178178
</button>
179179

180+
<button
181+
class="diagnostics-toggle github-pr-context-disconnect"
182+
id="github-pr-context-disconnect"
183+
type="button"
184+
aria-label="Disconnect active pull request context"
185+
title="Disconnect active pull request context"
186+
hidden
187+
>
188+
<svg
189+
class="github-pr-context-disconnect__icon"
190+
xmlns="http://www.w3.org/2000/svg"
191+
viewBox="0 0 24 24"
192+
aria-hidden="true"
193+
>
194+
<path
195+
d="M9.036 7.976a.75.75 0 0 0-1.06 1.06L10.939 12l-2.963 2.963a.75.75 0 1 0 1.06 1.06L12 13.06l2.963 2.964a.75.75 0 0 0 1.061-1.06L13.061 12l2.963-2.964a.75.75 0 1 0-1.06-1.06L12 10.939 9.036 7.976Z"
196+
></path>
197+
<path
198+
d="M12 1c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1ZM2.5 12a9.5 9.5 0 0 0 9.5 9.5 9.5 9.5 0 0 0 9.5-9.5A9.5 9.5 0 0 0 12 2.5 9.5 9.5 0 0 0 2.5 12Z"
199+
></path>
200+
</svg>
201+
<span class="github-pr-context-disconnect__label">Disconnect</span>
202+
</button>
203+
180204
<button
181205
class="diagnostics-toggle ai-chat-toggle"
182206
id="ai-chat-toggle"
@@ -185,16 +209,19 @@ <h1>
185209
aria-controls="ai-chat-drawer"
186210
hidden
187211
>
188-
<span
189-
class="ai-chat-toggle__emoji ai-chat-toggle__emoji--dark"
190-
aria-hidden="true"
191-
></span
192-
>
193-
<span
194-
class="ai-chat-toggle__emoji ai-chat-toggle__emoji--light"
212+
<svg
213+
class="ai-chat-toggle__icon"
214+
xmlns="http://www.w3.org/2000/svg"
215+
viewBox="0 0 24 24"
195216
aria-hidden="true"
196-
>🤖</span
197217
>
218+
<path
219+
d="M1.75 1h12.5c.966 0 1.75.784 1.75 1.75v9.5A1.75 1.75 0 0 1 14.25 14H8.061l-2.574 2.573A1.458 1.458 0 0 1 3 15.543V14H1.75A1.75 1.75 0 0 1 0 12.25v-9.5C0 1.784.784 1 1.75 1ZM1.5 2.75v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25Z"
220+
></path>
221+
<path
222+
d="M22.5 8.75a.25.25 0 0 0-.25-.25h-3.5a.75.75 0 0 1 0-1.5h3.5c.966 0 1.75.784 1.75 1.75v9.5A1.75 1.75 0 0 1 22.25 20H21v1.543a1.457 1.457 0 0 1-2.487 1.03L15.939 20H10.75A1.75 1.75 0 0 1 9 18.25v-1.465a.75.75 0 0 1 1.5 0v1.465c0 .138.112.25.25.25h5.5a.75.75 0 0 1 .53.22l2.72 2.72v-2.19a.75.75 0 0 1 .75-.75h2a.25.25 0 0 0 .25-.25v-9.5Z"
223+
></path>
224+
</svg>
198225
<span>Chat</span>
199226
</button>
200227
</div>

src/modules/github-pr-drawer.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,45 @@ export const createGitHubPrDrawer = ({
15241524
setOpen,
15251525
isOpen: () => open,
15261526
getActivePrContext: () => getCurrentActivePrContext(),
1527+
disconnectActivePrContext: () => {
1528+
const repository = getSelectedRepositoryObject()
1529+
const repositoryFullName = getRepositoryFullName(repository)
1530+
if (!repositoryFullName) {
1531+
return { reference: '' }
1532+
}
1533+
1534+
const savedConfig = readRepositoryPrConfig(repositoryFullName)
1535+
const previousActiveContext =
1536+
savedConfig?.isActivePr === true
1537+
? {
1538+
repositoryFullName,
1539+
pullRequestNumber:
1540+
typeof savedConfig.pullRequestNumber === 'number' &&
1541+
Number.isFinite(savedConfig.pullRequestNumber)
1542+
? savedConfig.pullRequestNumber
1543+
: parsePullRequestNumberFromUrl(savedConfig.pullRequestUrl),
1544+
}
1545+
: null
1546+
1547+
if (Object.keys(savedConfig).length > 0) {
1548+
saveRepositoryPrConfig({
1549+
repositoryFullName,
1550+
config: {
1551+
...savedConfig,
1552+
isActivePr: false,
1553+
},
1554+
})
1555+
}
1556+
1557+
lastActiveContentSyncKey = ''
1558+
abortPendingActiveContentSyncRequest()
1559+
setSubmitButtonLabel()
1560+
emitActivePrContextChange()
1561+
1562+
return {
1563+
reference: formatActivePrReference(previousActiveContext),
1564+
}
1565+
},
15271566
clearActivePrContext: () => {
15281567
const repository = getSelectedRepositoryObject()
15291568
const repositoryFullName = getRepositoryFullName(repository)

0 commit comments

Comments
 (0)