Skip to content

Commit 61a9ee2

Browse files
fix(workspaces): persist post-push sync baseline to IDB and stabilize active PR context switching.
1 parent 5ef284d commit 61a9ee2

10 files changed

Lines changed: 617 additions & 42 deletions

playwright/github-pr-drawer/active-context-switch-debounce-essential.spec.ts

Lines changed: 417 additions & 8 deletions
Large diffs are not rendered by default.

src/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,7 @@ const githubWorkflows = createGitHubWorkflowsSetup({
11031103
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
11041104
setActiveWorkspaceRecordId,
11051105
setActiveWorkspaceCreatedAt: value => (activeWorkspaceCreatedAt = value),
1106+
buildWorkspaceRecordSnapshot,
11061107
listLocalContextRecords,
11071108
refreshLocalContextOptions,
11081109
applyWorkspaceRecord,

src/modules/app-core/github-workflows-setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const createGitHubWorkflowsSetup = ({
2222
getActiveWorkspaceRecordId: workspace.getActiveWorkspaceRecordId,
2323
setActiveWorkspaceRecordId: workspace.setActiveWorkspaceRecordId,
2424
setActiveWorkspaceCreatedAt: workspace.setActiveWorkspaceCreatedAt,
25+
buildWorkspaceRecordSnapshot: workspace.buildWorkspaceRecordSnapshot,
2526
listLocalContextRecords: workspace.listLocalContextRecords,
2627
refreshLocalContextOptions: workspace.refreshLocalContextOptions,
2728
applyWorkspaceRecord: workspace.applyWorkspaceRecord,

src/modules/app-core/github-workflows.js

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const initializeGitHubWorkflows = ({
4949
getActiveWorkspaceRecordId,
5050
setActiveWorkspaceRecordId,
5151
setActiveWorkspaceCreatedAt,
52+
buildWorkspaceRecordSnapshot,
5253
listLocalContextRecords,
5354
refreshLocalContextOptions,
5455
applyWorkspaceRecord,
@@ -153,6 +154,29 @@ const initializeGitHubWorkflows = ({
153154
return true
154155
}
155156

157+
const persistActiveWorkspaceSnapshot = async () => {
158+
if (typeof buildWorkspaceRecordSnapshot !== 'function') {
159+
return null
160+
}
161+
162+
const activeWorkspaceRecordId =
163+
typeof getActiveWorkspaceRecordId === 'function' ? getActiveWorkspaceRecordId() : ''
164+
165+
const snapshot =
166+
typeof activeWorkspaceRecordId === 'string' && activeWorkspaceRecordId.trim()
167+
? buildWorkspaceRecordSnapshot({ recordId: activeWorkspaceRecordId })
168+
: buildWorkspaceRecordSnapshot()
169+
170+
if (!snapshot || typeof snapshot !== 'object') {
171+
return null
172+
}
173+
174+
const savedWorkspaceRecord = await workspaceStorage.upsertWorkspace(snapshot)
175+
setActiveWorkspaceRecordId(savedWorkspaceRecord.id)
176+
setActiveWorkspaceCreatedAt(savedWorkspaceRecord.createdAt ?? null)
177+
return savedWorkspaceRecord
178+
}
179+
156180
const prEditorSyncController = createGitHubPrEditorSyncController({
157181
shouldApplySyncResult: shouldApplyActivePrEditorSync,
158182
})
@@ -230,6 +254,28 @@ const initializeGitHubWorkflows = ({
230254
onPrContextStateChange(githubAiContextState.activePrContext)
231255
}
232256

257+
const activeContextSyncKey = getActivePrContextSyncKey(
258+
githubAiContextState.activePrContext,
259+
)
260+
if (activeContextSyncKey && activeContextSyncKey === getActivePrEditorSyncKey()) {
261+
prContextUi.markActivePrEditorContentSynced()
262+
}
263+
264+
const message = url
265+
? `Pull request opened: ${url}`
266+
: 'Pull request opened successfully.'
267+
if (shouldReconcileWorkspaceUpdatesForRepository(repositoryFullName)) {
268+
reconcileWorkspaceTabsWithPushUpdates(fileUpdates)
269+
}
270+
271+
if (typeof flushWorkspaceSave === 'function') {
272+
try {
273+
await flushWorkspaceSave({ preserveRecordId: true })
274+
} catch {
275+
/* Save failures are already surfaced through saver onError. */
276+
}
277+
}
278+
233279
const activeWorkspaceRecordId =
234280
typeof getActiveWorkspaceRecordId === 'function'
235281
? getActiveWorkspaceRecordId()
@@ -264,29 +310,31 @@ const initializeGitHubWorkflows = ({
264310

265311
setActiveWorkspaceRecordId(savedWorkspaceRecord.id)
266312
setActiveWorkspaceCreatedAt(savedWorkspaceRecord.createdAt ?? null)
267-
await refreshLocalContextOptions()
268313
}
269314
}
270315

271-
const activeContextSyncKey = getActivePrContextSyncKey(
272-
githubAiContextState.activePrContext,
273-
)
274-
if (activeContextSyncKey && activeContextSyncKey === getActivePrEditorSyncKey()) {
275-
prContextUi.markActivePrEditorContentSynced()
276-
}
277-
278-
const message = url
279-
? `Pull request opened: ${url}`
280-
: 'Pull request opened successfully.'
281-
if (shouldReconcileWorkspaceUpdatesForRepository(repositoryFullName)) {
282-
reconcileWorkspaceTabsWithPushUpdates(fileUpdates)
283-
}
316+
await refreshLocalContextOptions()
284317
showAppToast(message)
285318
},
286-
onPullRequestCommitPushed: ({ repositoryFullName, branch, fileUpdates }) => {
319+
onPullRequestCommitPushed: async ({ repositoryFullName, branch, fileUpdates }) => {
287320
if (shouldReconcileWorkspaceUpdatesForRepository(repositoryFullName)) {
288321
reconcileWorkspaceTabsWithPushUpdates(fileUpdates)
289322
}
323+
324+
try {
325+
await persistActiveWorkspaceSnapshot()
326+
} catch {
327+
/* Fall back to debounced saver flush below. */
328+
}
329+
330+
if (typeof flushWorkspaceSave === 'function') {
331+
try {
332+
await flushWorkspaceSave({ preserveRecordId: true })
333+
} catch {
334+
/* Save failures are already surfaced through saver onError. */
335+
}
336+
}
337+
290338
const fileCount = Array.isArray(fileUpdates) ? fileUpdates.length : 0
291339
const message =
292340
fileCount > 0

src/modules/app-core/workspace-editor-helpers.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,28 @@ const createWorkspaceEditorHelpers = ({
2424
setSuppressEditorChangeSideEffects,
2525
editorPool,
2626
}) => {
27+
const isTabEditedForDisplay = tab => {
28+
if (!tab || typeof tab !== 'object' || tab.isDirty !== true) {
29+
return false
30+
}
31+
32+
const nextContent = typeof tab.content === 'string' ? tab.content : ''
33+
const syncedContent = typeof tab.syncedContent === 'string' ? tab.syncedContent : null
34+
35+
if (syncedContent !== null) {
36+
return nextContent !== syncedContent
37+
}
38+
39+
const hasSyncTimestamp =
40+
typeof tab.syncedAt === 'number' &&
41+
Number.isFinite(tab.syncedAt) &&
42+
tab.syncedAt > 0
43+
const hasSyncSha =
44+
typeof tab.lastSyncedRemoteSha === 'string' && tab.lastSyncedRemoteSha.trim()
45+
46+
return Boolean(hasSyncTimestamp || hasSyncSha)
47+
}
48+
2749
const getWorkspaceTabByKind = kind => {
2850
const tabs = workspaceTabsState.getTabs()
2951
const normalizedKind = kind === 'styles' ? 'styles' : 'component'
@@ -59,7 +81,7 @@ const createWorkspaceEditorHelpers = ({
5981
typeof getShouldShowEditedDesign === 'function'
6082
? Boolean(getShouldShowEditedDesign())
6183
: true
62-
const isDirty = shouldShowEditedDesign && Boolean(tab?.isDirty)
84+
const isDirty = shouldShowEditedDesign && isTabEditedForDisplay(tab)
6385
dirtyStatusLabel.hidden = !isDirty
6486
if (isDirty) {
6587
dirtyStatusLabel.removeAttribute('aria-hidden')

src/modules/app-core/workspace-sync-controller.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ const createWorkspaceSyncController = ({
1919
getRenderModeValue,
2020
normalizeRenderMode,
2121
}) => {
22+
const resolveCanonicalDirtyState = ({ tab, content }) => {
23+
const syncedContent = toWorkspaceSyncedContent(tab?.syncedContent)
24+
if (syncedContent !== null) {
25+
return content !== syncedContent
26+
}
27+
28+
return Boolean(tab?.isDirty)
29+
}
30+
2231
const buildWorkspaceTabsSnapshot = () => {
2332
const activeTabId = workspaceTabsState.getActiveTabId()
2433
return workspaceTabsState.getTabs().map(tab => {
@@ -35,12 +44,17 @@ const createWorkspaceSyncController = ({
3544

3645
const normalizedPath = normalizeWorkspacePathValue(currentPath)
3746
const targetPrFilePath = normalizedPath || getTabTargetPrFilePath(tab) || null
47+
const canonicalDirtyState = resolveCanonicalDirtyState({
48+
tab,
49+
content: currentContent,
50+
})
3851

3952
return {
4053
...tab,
4154
path: currentPath,
4255
content: currentContent,
4356
syncedContent: toWorkspaceSyncedContent(tab?.syncedContent),
57+
isDirty: canonicalDirtyState,
4458
targetPrFilePath,
4559
isActive: activeTabId === tab.id,
4660
lastModified: Date.now(),

src/modules/app-core/workspace-tabs-renderer.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,28 @@ const createWorkspaceTabsRenderer = ({
2323
workspaceTabsShell,
2424
workspaceTabAddWrap,
2525
}) => {
26+
const isTabEditedForDisplay = tab => {
27+
if (!tab || typeof tab !== 'object' || tab.isDirty !== true) {
28+
return false
29+
}
30+
31+
const nextContent = typeof tab.content === 'string' ? tab.content : ''
32+
const syncedContent = typeof tab.syncedContent === 'string' ? tab.syncedContent : null
33+
34+
if (syncedContent !== null) {
35+
return nextContent !== syncedContent
36+
}
37+
38+
const hasSyncTimestamp =
39+
typeof tab.syncedAt === 'number' &&
40+
Number.isFinite(tab.syncedAt) &&
41+
tab.syncedAt > 0
42+
const hasSyncSha =
43+
typeof tab.lastSyncedRemoteSha === 'string' && tab.lastSyncedRemoteSha.trim()
44+
45+
return Boolean(hasSyncTimestamp || hasSyncSha)
46+
}
47+
2648
const clearWorkspaceTabDragState = () => {
2749
setDraggedWorkspaceTabId('')
2850
setDragOverWorkspaceTabId('')
@@ -53,7 +75,7 @@ const createWorkspaceTabsRenderer = ({
5375
for (const tab of tabs) {
5476
const isActive = tab.id === activeTabId
5577
const isRenaming = getWorkspaceTabRenameState().tabId === tab.id
56-
const isEdited = shouldShowEditedDesign && tab.isDirty
78+
const isEdited = shouldShowEditedDesign && isTabEditedForDisplay(tab)
5779
const editedSuffix = isEdited ? ' (Edited)' : ''
5880
const tabContainer = document.createElement('li')
5981
tabContainer.className = 'workspace-tab'
@@ -235,7 +257,7 @@ const createWorkspaceTabsRenderer = ({
235257
tabContainer.append(metaBadge)
236258
}
237259

238-
if (shouldShowEditedDesign && tab.isDirty) {
260+
if (shouldShowEditedDesign && isTabEditedForDisplay(tab)) {
239261
const dirtyBadge = document.createElement('span')
240262
dirtyBadge.className = 'workspace-tab__dirty-indicator'
241263
dirtyBadge.setAttribute('aria-hidden', 'true')

src/modules/github/pr/drawer/controller/create-controller.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const createGitHubPrDrawer = ({
8080
pendingContextVerifyRequestKey: '',
8181
pendingContextVerifyPromise: null,
8282
lastSyncedRepositoryFullName: '',
83+
lastSyncedActivePrContextKey: '',
8384
lastActiveContentSyncKey: '',
8485
baseBranchesByRepository: new Map(),
8586
activePrContextByRepository: new Map(),
@@ -336,6 +337,9 @@ export const createGitHubPrDrawer = ({
336337
return false
337338
}
338339

340+
contextHandlers.abortPendingContextVerifyRequest()
341+
contextHandlers.abortPendingActiveContentSyncRequest()
342+
339343
setRepositoryActivePrContext({
340344
repositoryFullName: targetRepositoryFullName,
341345
activeContext,

src/modules/github/pr/drawer/controller/repository-form.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,22 @@ export const createRepositoryFormHandlers = ({
244244
Boolean(repositoryFullName) &&
245245
repositoryFullName !== state.lastSyncedRepositoryFullName
246246
const activeContext = getCurrentActivePrContext()
247+
const activeContextSyncKey = activeContext
248+
? [
249+
toSafeText(activeContext.baseBranch),
250+
sanitizeBranchPart(activeContext.headBranch),
251+
toSafeText(activeContext.prTitle),
252+
String(
253+
typeof activeContext.pullRequestNumber === 'number' &&
254+
Number.isFinite(activeContext.pullRequestNumber)
255+
? activeContext.pullRequestNumber
256+
: '',
257+
),
258+
toSafeText(activeContext.pullRequestUrl),
259+
].join('|')
260+
: ''
261+
const activeContextChanged =
262+
activeContextSyncKey !== state.lastSyncedActivePrContextKey
247263

248264
const baseBranch =
249265
toSafeText(activeContext?.baseBranch) ||
@@ -257,7 +273,13 @@ export const createRepositoryFormHandlers = ({
257273
const currentHeadBranch = toSafeText(headBranchInput.value)
258274

259275
if (activeHeadBranch) {
260-
if (resetAll || resetBranch || repositoryChanged || !currentHeadBranch) {
276+
if (
277+
resetAll ||
278+
resetBranch ||
279+
repositoryChanged ||
280+
activeContextChanged ||
281+
!currentHeadBranch
282+
) {
261283
setElementValueAndPersist(headBranchInput, activeHeadBranch)
262284
}
263285
} else if (!currentHeadBranch) {
@@ -266,13 +288,23 @@ export const createRepositoryFormHandlers = ({
266288
}
267289

268290
if (prTitleInput instanceof HTMLInputElement) {
269-
if (resetAll || repositoryChanged || !toSafeText(prTitleInput.value)) {
291+
if (
292+
resetAll ||
293+
repositoryChanged ||
294+
activeContextChanged ||
295+
!toSafeText(prTitleInput.value)
296+
) {
270297
setElementValueAndPersist(prTitleInput, toSafeText(activeContext?.prTitle))
271298
}
272299
}
273300

274301
if (prBodyInput instanceof HTMLTextAreaElement) {
275-
if (resetAll || repositoryChanged || !toSafeText(prBodyInput.value)) {
302+
if (
303+
resetAll ||
304+
repositoryChanged ||
305+
activeContextChanged ||
306+
!toSafeText(prBodyInput.value)
307+
) {
276308
prBodyInput.value =
277309
typeof activeContext?.prBody === 'string' ? activeContext.prBody : ''
278310
}
@@ -289,6 +321,7 @@ export const createRepositoryFormHandlers = ({
289321
}
290322

291323
state.lastSyncedRepositoryFullName = repositoryFullName
324+
state.lastSyncedActivePrContextKey = activeContextSyncKey
292325
}
293326

294327
const refreshContextUi = () => {

0 commit comments

Comments
 (0)