Skip to content

Commit d3dfd9b

Browse files
feat: workspace status bar. (#109)
1 parent 7d4f968 commit d3dfd9b

7 files changed

Lines changed: 276 additions & 55 deletions

File tree

playwright/github-byot-ai.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ test('chat becomes available after token connect', async ({ page }) => {
4848
await expect(page.getByRole('button', { name: 'Chat' })).toBeVisible()
4949
})
5050

51+
test('workspace context status is visible only after PAT connect', async ({ page }) => {
52+
await waitForAppReady(page)
53+
54+
const workspaceContextStatus = page.locator('#workspace-context-status')
55+
await expect(workspaceContextStatus).toBeHidden()
56+
57+
await connectByotWithSingleRepo(page)
58+
await expect(workspaceContextStatus).toBeVisible()
59+
})
60+
5161
test('BYOT controls render with default app entry', async ({ page }) => {
5262
await waitForAppReady(page, appEntryPath)
5363

src/app.js

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import { createPersistedActivePrContextGetter } from './modules/app-core/persist
4949
import { createWorkspacePrSessionHandoffController } from './modules/app-core/workspace-pr-session-handoff-controller.js'
5050
import { persistClosedPrContextRecords } from './modules/app-core/pr-context-records.js'
5151
import { createPrContextStateChangeHandler } from './modules/app-core/pr-context-state-change-handler.js'
52+
import { createWorkspaceContextStatusController } from './modules/app-core/workspace-context-status-controller.js'
53+
import { createWorkspaceRecordAppliedHandler } from './modules/app-core/workspace-record-applied-handler.js'
5254
import { createDiagnosticsUiController } from './modules/diagnostics/diagnostics-ui.js'
5355
import { createGitHubChatDrawer } from './modules/github/chat-drawer/drawer.js'
5456
import { createGitHubByotControls } from './modules/github/byot-controls.js'
@@ -165,6 +167,7 @@ const appThemeButtons = document.querySelectorAll('[data-app-theme]')
165167
const workspaceTabsShell = document.getElementById('workspace-tabs-shell')
166168
const workspaceTabsStrip = document.getElementById('workspace-tabs-strip')
167169
const workspaceTabAddWrap = document.getElementById('workspace-tab-add-wrap')
170+
const workspaceContextStatus = document.getElementById('workspace-context-status')
168171
const workspaceTabAddButton = document.getElementById('workspace-tab-add')
169172
const workspaceTabAddMenu = document.getElementById('workspace-tab-add-menu')
170173
const workspaceTabAddModule = document.getElementById('workspace-tab-add-module')
@@ -424,11 +427,17 @@ let workspacePrNumber = null
424427
let workspaceRepositoryFullName = ''
425428
let workspaceScopeMarker = 'local'
426429
let hasObservedActivePrContextInSession = false
430+
let workspaceContextStatusController = {
431+
render: () => {},
432+
renderForRepositoryChange: () => {},
433+
syncTokenState: () => {},
434+
syncWritableRepositoriesState: () => {},
435+
}
427436

428437
const toWorkspaceScopeMarker = value => (value === 'repository' ? 'repository' : 'local')
429-
430438
const setWorkspaceScopeMarker = nextScope => {
431439
workspaceScopeMarker = toWorkspaceScopeMarker(nextScope)
440+
workspaceContextStatusController.render()
432441
}
433442

434443
const toPullRequestNumber = value => {
@@ -445,6 +454,7 @@ const setActiveWorkspaceRecordId = nextValue => {
445454
workspaceRepositoryFullName = ''
446455
workspaceScopeMarker = 'local'
447456
}
457+
workspaceContextStatusController.render()
448458
}
449459

450460
let chatDrawerController = {
@@ -509,17 +519,28 @@ const byotControls = createGitHubByotControls({
509519
prDrawerController.setSelectedRepository(repository)
510520
hasObservedActivePrContextInSession = false
511521
prDrawerController.syncRepositories()
522+
workspaceContextStatusController.renderForRepositoryChange()
512523
},
513-
onWritableRepositoriesChange: ({ repositories, selectedRepository }) => {
524+
onWritableRepositoriesChange: ({
525+
repositories,
526+
selectedRepository,
527+
isLoadingRepositories,
528+
}) => {
514529
githubAiContextState.writableRepositories = Array.isArray(repositories)
515530
? [...repositories]
516531
: []
517532

533+
workspaceContextStatusController.syncWritableRepositoriesState({
534+
token: githubAiContextState.token,
535+
isLoadingRepositories,
536+
})
537+
518538
if (selectedRepository || githubAiContextState.selectedRepository) {
519539
githubAiContextState.selectedRepository = selectedRepository ?? null
520540
chatDrawerController.setSelectedRepository(selectedRepository)
521541
prDrawerController.setSelectedRepository(selectedRepository)
522542
prDrawerController.syncRepositories()
543+
workspaceContextStatusController.renderForRepositoryChange()
523544
return
524545
}
525546

@@ -535,6 +556,8 @@ const byotControls = createGitHubByotControls({
535556
prDrawerController.setSelectedRepository(synchronizedRepository)
536557
prDrawerController.syncRepositories()
537558
}
559+
560+
workspaceContextStatusController.renderForRepositoryChange()
538561
},
539562
onTokenDeleteRequest: onConfirm => {
540563
confirmAction({
@@ -546,6 +569,7 @@ const byotControls = createGitHubByotControls({
546569
},
547570
onTokenChange: token => {
548571
githubAiContextState.token = token
572+
workspaceContextStatusController.syncTokenState(token)
549573
prContextUi.syncAiChatTokenVisibility(token)
550574
chatDrawerController.setToken(token)
551575
prDrawerController.setToken(token)
@@ -575,6 +599,19 @@ const getCurrentSelectedRepositoryFullName = () => {
575599
return ''
576600
}
577601

602+
workspaceContextStatusController = createWorkspaceContextStatusController({
603+
statusNode: workspaceContextStatus,
604+
toNonEmptyWorkspaceText,
605+
getWorkspacePrTitle: () => githubPrTitle?.value,
606+
getWorkspaceHeadBranch: () => githubPrHeadBranch?.value,
607+
getWorkspaceScopeMarker: () => workspaceScopeMarker,
608+
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
609+
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
610+
getSelectedRepositoryFullName: getCurrentSelectedRepositoryFullName,
611+
})
612+
613+
workspaceContextStatusController.render()
614+
578615
const getPersistedActivePrContext = createPersistedActivePrContextGetter({
579616
getCurrentSelectedRepositoryFullName,
580617
getWorkspacePrContextState: () => workspacePrContextState,
@@ -733,6 +770,20 @@ const reconcileWorkspaceTabsWithEditorSync = ({ tabTargets } = {}) =>
733770
const buildWorkspaceRecordSnapshot = ({ recordId } = {}) =>
734771
workspaceSyncController.buildWorkspaceRecordSnapshot({ recordId })
735772

773+
const setWorkspaceRepositoryFullName = value => {
774+
workspaceRepositoryFullName = toNonEmptyWorkspaceText(value)
775+
workspaceContextStatusController.render()
776+
}
777+
const onWorkspaceRecordApplied = createWorkspaceRecordAppliedHandler({
778+
getPrDrawerController: () => prDrawerController,
779+
setWorkspaceRepositoryFullName,
780+
byotControls,
781+
getGithubPrBodyValue: () =>
782+
typeof githubPrBody?.value === 'string' ? githubPrBody.value : '',
783+
normalizeRenderMode,
784+
getStyleModeValue: () => styleMode.value,
785+
})
786+
736787
const {
737788
workspaceSaveController,
738789
listLocalContextRecords,
@@ -821,49 +872,7 @@ const {
821872
getWorkspaceTabByKind,
822873
makeUniqueTabPath,
823874
createWorkspaceTabId,
824-
onWorkspaceRecordApplied: workspace => {
825-
if (!workspace || typeof workspace !== 'object') {
826-
return
827-
}
828-
829-
prDrawerController.clearSelectedRepositoryActivePrContext({ resetForm: false })
830-
831-
const nextWorkspaceRepositoryFullName =
832-
typeof workspace.repo === 'string' ? workspace.repo.trim() : ''
833-
if (nextWorkspaceRepositoryFullName) {
834-
workspaceRepositoryFullName = nextWorkspaceRepositoryFullName
835-
byotControls.setSelectedRepository(nextWorkspaceRepositoryFullName)
836-
}
837-
838-
const state =
839-
typeof workspace.prContextState === 'string'
840-
? workspace.prContextState.trim().toLowerCase()
841-
: ''
842-
const shouldHydratePrContext = state === 'active'
843-
if (!shouldHydratePrContext) {
844-
return
845-
}
846-
847-
prDrawerController.hydrateActivePrContext(
848-
{
849-
baseBranch: typeof workspace.base === 'string' ? workspace.base : '',
850-
headBranch: typeof workspace.head === 'string' ? workspace.head : '',
851-
prTitle: typeof workspace.prTitle === 'string' ? workspace.prTitle : '',
852-
prBody: typeof githubPrBody?.value === 'string' ? githubPrBody.value : '',
853-
pullRequestNumber:
854-
typeof workspace.prNumber === 'number' && Number.isFinite(workspace.prNumber)
855-
? workspace.prNumber
856-
: null,
857-
pullRequestUrl: '',
858-
renderMode: normalizeRenderMode(workspace.renderMode),
859-
styleMode: styleMode.value,
860-
},
861-
{
862-
repositoryFullName:
863-
typeof workspace.repo === 'string' ? workspace.repo.trim() : '',
864-
},
865-
)
866-
},
875+
onWorkspaceRecordApplied,
867876
})
868877

869878
const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
@@ -886,9 +895,7 @@ const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
886895
setActiveWorkspaceRecordId,
887896
setActiveWorkspaceCreatedAt: value => (activeWorkspaceCreatedAt = value),
888897
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
889-
setWorkspaceRepositoryFullName: value => {
890-
workspaceRepositoryFullName = toNonEmptyWorkspaceText(value)
891-
},
898+
setWorkspaceRepositoryFullName,
892899
setWorkspaceScopeMarker,
893900
setHeadBranchValue: value => {
894901
if (githubPrHeadBranch) {
@@ -949,14 +956,13 @@ const setWorkspacePrContextState = nextState => {
949956
if (typeof nextState !== 'string' || !nextState.trim()) {
950957
return
951958
}
952-
953959
workspacePrContextState = nextState.trim()
960+
workspaceContextStatusController.render()
954961
}
955962

956963
const setWorkspacePrNumber = nextValue => {
957964
workspacePrNumber = toPullRequestNumber(nextValue)
958965
}
959-
960966
const persistWorkspacePrContextState = nextState => {
961967
setWorkspacePrContextState(nextState)
962968
queueWorkspaceSave({ preserveRecordId: true })
@@ -1019,7 +1025,7 @@ const onPrContextStateChange = createPrContextStateChangeHandler({
10191025
parsePullRequestNumberFromUrl,
10201026
getCurrentSelectedRepositoryFullName,
10211027
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
1022-
setWorkspaceRepositoryFullName: value => (workspaceRepositoryFullName = value),
1028+
setWorkspaceRepositoryFullName,
10231029
getWorkspacePrContextState: () => workspacePrContextState,
10241030
getHasObservedActivePrContextInSession: () => hasObservedActivePrContextInSession,
10251031
setHasObservedActivePrContextInSession: value =>

src/index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,15 @@ <h1>
343343
</nav>
344344
</div>
345345

346+
<div
347+
class="app-grid-workspace-context-status"
348+
id="workspace-context-status"
349+
hidden
350+
role="status"
351+
aria-live="polite"
352+
aria-atomic="true"
353+
></div>
354+
346355
<div class="workspace-editors-stack">
347356
<div class="panels-stack panels-stack--editors">
348357
<section
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const hasTokenValue = token => typeof token === 'string' && token.trim().length > 0
2+
3+
const createWorkspaceContextStatusController = ({
4+
statusNode,
5+
toNonEmptyWorkspaceText,
6+
getWorkspacePrTitle,
7+
getWorkspaceHeadBranch,
8+
getWorkspaceScopeMarker,
9+
getActiveWorkspaceRecordId,
10+
getWorkspaceRepositoryFullName,
11+
getSelectedRepositoryFullName,
12+
}) => {
13+
let hasValidatedGitHubPat = false
14+
let hasCompletedRepositoryLoad = false
15+
const appGrid =
16+
statusNode instanceof HTMLElement ? statusNode.closest('.app-grid') : null
17+
18+
const getWorkspaceName = () => {
19+
const prTitle = toNonEmptyWorkspaceText(getWorkspacePrTitle?.())
20+
if (prTitle) {
21+
return prTitle
22+
}
23+
24+
const headBranch = toNonEmptyWorkspaceText(getWorkspaceHeadBranch?.())
25+
if (headBranch) {
26+
return headBranch
27+
}
28+
29+
return toNonEmptyWorkspaceText(getActiveWorkspaceRecordId?.()) || 'unknown'
30+
}
31+
32+
const render = () => {
33+
if (!(statusNode instanceof HTMLElement)) {
34+
return
35+
}
36+
37+
if (appGrid instanceof HTMLElement) {
38+
appGrid.classList.toggle(
39+
'app-grid--workspace-context-visible',
40+
hasValidatedGitHubPat,
41+
)
42+
}
43+
44+
statusNode.toggleAttribute('hidden', !hasValidatedGitHubPat)
45+
if (!hasValidatedGitHubPat) {
46+
return
47+
}
48+
49+
const workspaceName = getWorkspaceName()
50+
const workspaceScope =
51+
toNonEmptyWorkspaceText(getWorkspaceScopeMarker?.()).toLowerCase() || 'local'
52+
const repository =
53+
workspaceScope === 'local'
54+
? 'local'
55+
: toNonEmptyWorkspaceText(getWorkspaceRepositoryFullName?.()) ||
56+
toNonEmptyWorkspaceText(getSelectedRepositoryFullName?.()) ||
57+
'unknown'
58+
59+
statusNode.textContent = `${workspaceName}${repository}`
60+
}
61+
62+
const renderForRepositoryChange = () => {
63+
render()
64+
}
65+
66+
const syncTokenState = token => {
67+
if (!hasTokenValue(token)) {
68+
hasValidatedGitHubPat = false
69+
hasCompletedRepositoryLoad = false
70+
} else if (hasCompletedRepositoryLoad) {
71+
hasValidatedGitHubPat = true
72+
}
73+
74+
render()
75+
}
76+
77+
const syncWritableRepositoriesState = ({ token, isLoadingRepositories = false }) => {
78+
if (!isLoadingRepositories) {
79+
hasCompletedRepositoryLoad = true
80+
}
81+
82+
if (hasTokenValue(token) && !isLoadingRepositories) {
83+
hasValidatedGitHubPat = true
84+
}
85+
86+
render()
87+
}
88+
89+
return {
90+
render,
91+
renderForRepositoryChange,
92+
syncTokenState,
93+
syncWritableRepositoriesState,
94+
}
95+
}
96+
97+
export { createWorkspaceContextStatusController }

0 commit comments

Comments
 (0)