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
10 changes: 10 additions & 0 deletions playwright/github-byot-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ test('chat becomes available after token connect', async ({ page }) => {
await expect(page.getByRole('button', { name: 'Chat' })).toBeVisible()
})

test('workspace context status is visible only after PAT connect', async ({ page }) => {
await waitForAppReady(page)

const workspaceContextStatus = page.locator('#workspace-context-status')
await expect(workspaceContextStatus).toBeHidden()

await connectByotWithSingleRepo(page)
await expect(workspaceContextStatus).toBeVisible()
})

test('BYOT controls render with default app entry', async ({ page }) => {
await waitForAppReady(page, appEntryPath)

Expand Down
108 changes: 57 additions & 51 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import { createPersistedActivePrContextGetter } from './modules/app-core/persist
import { createWorkspacePrSessionHandoffController } from './modules/app-core/workspace-pr-session-handoff-controller.js'
import { persistClosedPrContextRecords } from './modules/app-core/pr-context-records.js'
import { createPrContextStateChangeHandler } from './modules/app-core/pr-context-state-change-handler.js'
import { createWorkspaceContextStatusController } from './modules/app-core/workspace-context-status-controller.js'
import { createWorkspaceRecordAppliedHandler } from './modules/app-core/workspace-record-applied-handler.js'
import { createDiagnosticsUiController } from './modules/diagnostics/diagnostics-ui.js'
import { createGitHubChatDrawer } from './modules/github/chat-drawer/drawer.js'
import { createGitHubByotControls } from './modules/github/byot-controls.js'
Expand Down Expand Up @@ -165,6 +167,7 @@ const appThemeButtons = document.querySelectorAll('[data-app-theme]')
const workspaceTabsShell = document.getElementById('workspace-tabs-shell')
const workspaceTabsStrip = document.getElementById('workspace-tabs-strip')
const workspaceTabAddWrap = document.getElementById('workspace-tab-add-wrap')
const workspaceContextStatus = document.getElementById('workspace-context-status')
const workspaceTabAddButton = document.getElementById('workspace-tab-add')
const workspaceTabAddMenu = document.getElementById('workspace-tab-add-menu')
const workspaceTabAddModule = document.getElementById('workspace-tab-add-module')
Expand Down Expand Up @@ -424,11 +427,17 @@ let workspacePrNumber = null
let workspaceRepositoryFullName = ''
let workspaceScopeMarker = 'local'
let hasObservedActivePrContextInSession = false
let workspaceContextStatusController = {
render: () => {},
renderForRepositoryChange: () => {},
syncTokenState: () => {},
syncWritableRepositoriesState: () => {},
}

const toWorkspaceScopeMarker = value => (value === 'repository' ? 'repository' : 'local')

const setWorkspaceScopeMarker = nextScope => {
workspaceScopeMarker = toWorkspaceScopeMarker(nextScope)
workspaceContextStatusController.render()
}

const toPullRequestNumber = value => {
Expand All @@ -445,6 +454,7 @@ const setActiveWorkspaceRecordId = nextValue => {
workspaceRepositoryFullName = ''
workspaceScopeMarker = 'local'
}
workspaceContextStatusController.render()
}

let chatDrawerController = {
Expand Down Expand Up @@ -509,17 +519,28 @@ const byotControls = createGitHubByotControls({
prDrawerController.setSelectedRepository(repository)
hasObservedActivePrContextInSession = false
prDrawerController.syncRepositories()
workspaceContextStatusController.renderForRepositoryChange()
},
onWritableRepositoriesChange: ({ repositories, selectedRepository }) => {
onWritableRepositoriesChange: ({
repositories,
selectedRepository,
isLoadingRepositories,
}) => {
githubAiContextState.writableRepositories = Array.isArray(repositories)
? [...repositories]
: []

workspaceContextStatusController.syncWritableRepositoriesState({
token: githubAiContextState.token,
isLoadingRepositories,
})

if (selectedRepository || githubAiContextState.selectedRepository) {
githubAiContextState.selectedRepository = selectedRepository ?? null
chatDrawerController.setSelectedRepository(selectedRepository)
prDrawerController.setSelectedRepository(selectedRepository)
prDrawerController.syncRepositories()
workspaceContextStatusController.renderForRepositoryChange()
return
}

Expand All @@ -535,6 +556,8 @@ const byotControls = createGitHubByotControls({
prDrawerController.setSelectedRepository(synchronizedRepository)
prDrawerController.syncRepositories()
}

workspaceContextStatusController.renderForRepositoryChange()
},
onTokenDeleteRequest: onConfirm => {
confirmAction({
Expand All @@ -546,6 +569,7 @@ const byotControls = createGitHubByotControls({
},
onTokenChange: token => {
githubAiContextState.token = token
workspaceContextStatusController.syncTokenState(token)
prContextUi.syncAiChatTokenVisibility(token)
chatDrawerController.setToken(token)
prDrawerController.setToken(token)
Expand Down Expand Up @@ -575,6 +599,19 @@ const getCurrentSelectedRepositoryFullName = () => {
return ''
}

workspaceContextStatusController = createWorkspaceContextStatusController({
statusNode: workspaceContextStatus,
toNonEmptyWorkspaceText,
getWorkspacePrTitle: () => githubPrTitle?.value,
getWorkspaceHeadBranch: () => githubPrHeadBranch?.value,
getWorkspaceScopeMarker: () => workspaceScopeMarker,
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
getSelectedRepositoryFullName: getCurrentSelectedRepositoryFullName,
})

workspaceContextStatusController.render()

const getPersistedActivePrContext = createPersistedActivePrContextGetter({
getCurrentSelectedRepositoryFullName,
getWorkspacePrContextState: () => workspacePrContextState,
Expand Down Expand Up @@ -733,6 +770,20 @@ const reconcileWorkspaceTabsWithEditorSync = ({ tabTargets } = {}) =>
const buildWorkspaceRecordSnapshot = ({ recordId } = {}) =>
workspaceSyncController.buildWorkspaceRecordSnapshot({ recordId })

const setWorkspaceRepositoryFullName = value => {
workspaceRepositoryFullName = toNonEmptyWorkspaceText(value)
workspaceContextStatusController.render()
}
const onWorkspaceRecordApplied = createWorkspaceRecordAppliedHandler({
getPrDrawerController: () => prDrawerController,
setWorkspaceRepositoryFullName,
byotControls,
getGithubPrBodyValue: () =>
typeof githubPrBody?.value === 'string' ? githubPrBody.value : '',
normalizeRenderMode,
getStyleModeValue: () => styleMode.value,
})

const {
workspaceSaveController,
listLocalContextRecords,
Expand Down Expand Up @@ -821,49 +872,7 @@ const {
getWorkspaceTabByKind,
makeUniqueTabPath,
createWorkspaceTabId,
onWorkspaceRecordApplied: workspace => {
if (!workspace || typeof workspace !== 'object') {
return
}

prDrawerController.clearSelectedRepositoryActivePrContext({ resetForm: false })

const nextWorkspaceRepositoryFullName =
typeof workspace.repo === 'string' ? workspace.repo.trim() : ''
if (nextWorkspaceRepositoryFullName) {
workspaceRepositoryFullName = nextWorkspaceRepositoryFullName
byotControls.setSelectedRepository(nextWorkspaceRepositoryFullName)
}

const state =
typeof workspace.prContextState === 'string'
? workspace.prContextState.trim().toLowerCase()
: ''
const shouldHydratePrContext = state === 'active'
if (!shouldHydratePrContext) {
return
}

prDrawerController.hydrateActivePrContext(
{
baseBranch: typeof workspace.base === 'string' ? workspace.base : '',
headBranch: typeof workspace.head === 'string' ? workspace.head : '',
prTitle: typeof workspace.prTitle === 'string' ? workspace.prTitle : '',
prBody: typeof githubPrBody?.value === 'string' ? githubPrBody.value : '',
pullRequestNumber:
typeof workspace.prNumber === 'number' && Number.isFinite(workspace.prNumber)
? workspace.prNumber
: null,
pullRequestUrl: '',
renderMode: normalizeRenderMode(workspace.renderMode),
styleMode: styleMode.value,
},
{
repositoryFullName:
typeof workspace.repo === 'string' ? workspace.repo.trim() : '',
},
)
},
onWorkspaceRecordApplied,
})

const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
Expand All @@ -886,9 +895,7 @@ const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
setActiveWorkspaceRecordId,
setActiveWorkspaceCreatedAt: value => (activeWorkspaceCreatedAt = value),
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
setWorkspaceRepositoryFullName: value => {
workspaceRepositoryFullName = toNonEmptyWorkspaceText(value)
},
setWorkspaceRepositoryFullName,
setWorkspaceScopeMarker,
setHeadBranchValue: value => {
if (githubPrHeadBranch) {
Expand Down Expand Up @@ -949,14 +956,13 @@ const setWorkspacePrContextState = nextState => {
if (typeof nextState !== 'string' || !nextState.trim()) {
return
}

workspacePrContextState = nextState.trim()
workspaceContextStatusController.render()
}

const setWorkspacePrNumber = nextValue => {
workspacePrNumber = toPullRequestNumber(nextValue)
}

const persistWorkspacePrContextState = nextState => {
setWorkspacePrContextState(nextState)
queueWorkspaceSave({ preserveRecordId: true })
Expand Down Expand Up @@ -1019,7 +1025,7 @@ const onPrContextStateChange = createPrContextStateChangeHandler({
parsePullRequestNumberFromUrl,
getCurrentSelectedRepositoryFullName,
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
setWorkspaceRepositoryFullName: value => (workspaceRepositoryFullName = value),
setWorkspaceRepositoryFullName,
getWorkspacePrContextState: () => workspacePrContextState,
getHasObservedActivePrContextInSession: () => hasObservedActivePrContextInSession,
setHasObservedActivePrContextInSession: value =>
Expand Down
9 changes: 9 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,15 @@ <h1>
</nav>
</div>

<div
class="app-grid-workspace-context-status"
id="workspace-context-status"
hidden
role="status"
aria-live="polite"
Comment thread
knightedcodemonkey marked this conversation as resolved.
aria-atomic="true"
></div>

<div class="workspace-editors-stack">
<div class="panels-stack panels-stack--editors">
<section
Expand Down
97 changes: 97 additions & 0 deletions src/modules/app-core/workspace-context-status-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const hasTokenValue = token => typeof token === 'string' && token.trim().length > 0

const createWorkspaceContextStatusController = ({
statusNode,
toNonEmptyWorkspaceText,
getWorkspacePrTitle,
getWorkspaceHeadBranch,
getWorkspaceScopeMarker,
getActiveWorkspaceRecordId,
getWorkspaceRepositoryFullName,
getSelectedRepositoryFullName,
}) => {
let hasValidatedGitHubPat = false
let hasCompletedRepositoryLoad = false
const appGrid =
statusNode instanceof HTMLElement ? statusNode.closest('.app-grid') : null

const getWorkspaceName = () => {
const prTitle = toNonEmptyWorkspaceText(getWorkspacePrTitle?.())
if (prTitle) {
return prTitle
}

const headBranch = toNonEmptyWorkspaceText(getWorkspaceHeadBranch?.())
if (headBranch) {
return headBranch
}

return toNonEmptyWorkspaceText(getActiveWorkspaceRecordId?.()) || 'unknown'
}

const render = () => {
if (!(statusNode instanceof HTMLElement)) {
return
}

if (appGrid instanceof HTMLElement) {
appGrid.classList.toggle(
'app-grid--workspace-context-visible',
hasValidatedGitHubPat,
)
}

statusNode.toggleAttribute('hidden', !hasValidatedGitHubPat)
if (!hasValidatedGitHubPat) {
return
}

const workspaceName = getWorkspaceName()
const workspaceScope =
toNonEmptyWorkspaceText(getWorkspaceScopeMarker?.()).toLowerCase() || 'local'
const repository =
workspaceScope === 'local'
? 'local'
: toNonEmptyWorkspaceText(getWorkspaceRepositoryFullName?.()) ||
toNonEmptyWorkspaceText(getSelectedRepositoryFullName?.()) ||
'unknown'

statusNode.textContent = `${workspaceName} • ${repository}`
}

const renderForRepositoryChange = () => {
render()
}

const syncTokenState = token => {
if (!hasTokenValue(token)) {
hasValidatedGitHubPat = false
hasCompletedRepositoryLoad = false
} else if (hasCompletedRepositoryLoad) {
hasValidatedGitHubPat = true
}

render()
}

const syncWritableRepositoriesState = ({ token, isLoadingRepositories = false }) => {
if (!isLoadingRepositories) {
hasCompletedRepositoryLoad = true
}

if (hasTokenValue(token) && !isLoadingRepositories) {
hasValidatedGitHubPat = true
}

render()
}
Comment thread
knightedcodemonkey marked this conversation as resolved.

return {
render,
renderForRepositoryChange,
syncTokenState,
syncWritableRepositoriesState,
}
}

export { createWorkspaceContextStatusController }
Loading
Loading