Skip to content

Commit 47cc8eb

Browse files
feat: show workspace context status by default, allow rename. (#130)
1 parent baf708e commit 47cc8eb

8 files changed

Lines changed: 267 additions & 63 deletions

File tree

playwright/github-byot-ai.spec.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,16 +303,103 @@ test('chat becomes available after token connect', async ({ page }) => {
303303
await expect(page.getByRole('button', { name: 'Chat' })).toBeVisible()
304304
})
305305

306-
test('workspace context status is visible only after PAT connect', async ({ page }) => {
306+
test('workspace context status stays visible without PAT and after PAT connect', async ({
307+
page,
308+
}) => {
307309
await waitForAppReady(page)
308310

309311
const workspaceContextStatus = page.locator('#workspace-context-status')
310-
await expect(workspaceContextStatus).toBeHidden()
312+
await expect(workspaceContextStatus).toBeVisible()
313+
await expect(workspaceContextStatus).toContainText('local')
311314

312315
await connectByotWithSingleRepo(page)
313316
await expect(workspaceContextStatus).toBeVisible()
314317
})
315318

319+
test('Local workspace can be renamed from Workspaces drawer', async ({ page }) => {
320+
const sourceWorkspaceId = 'local_workspace_rename_source'
321+
const targetWorkspaceId = 'local_workspace_rename_target'
322+
const originalTitle = 'Local rename original title'
323+
const renamedTitle = 'Local rename updated title'
324+
325+
await waitForAppReady(page)
326+
327+
await seedLocalWorkspaceContexts(page, [
328+
{
329+
id: sourceWorkspaceId,
330+
repo: '',
331+
workspaceScope: 'local',
332+
head: 'feat/local-rename-source',
333+
prTitle: originalTitle,
334+
prContextState: 'inactive',
335+
tabs: [
336+
{
337+
id: 'component',
338+
path: 'src/component.tsx',
339+
language: 'tsx',
340+
role: 'component',
341+
content: 'export const App = () => <main>rename source</main>',
342+
order: 0,
343+
source: 'workspace',
344+
dirty: false,
345+
},
346+
],
347+
activeTabId: 'component',
348+
},
349+
{
350+
id: targetWorkspaceId,
351+
repo: '',
352+
workspaceScope: 'local',
353+
head: 'feat/local-rename-target',
354+
prTitle: 'Local rename target title',
355+
prContextState: 'inactive',
356+
tabs: [
357+
{
358+
id: 'component',
359+
path: 'src/component.tsx',
360+
language: 'tsx',
361+
role: 'component',
362+
content: 'export const App = () => <main>rename target</main>',
363+
order: 0,
364+
source: 'workspace',
365+
dirty: false,
366+
},
367+
],
368+
activeTabId: 'component',
369+
},
370+
])
371+
372+
const workspacesToggle = page.getByRole('button', {
373+
name: 'Workspaces',
374+
exact: true,
375+
})
376+
await workspacesToggle.click()
377+
378+
const workspaceSelect = page.getByLabel('Stored workspace')
379+
const renameButton = page.getByRole('button', { name: 'Rename', exact: true })
380+
381+
await workspaceSelect.selectOption(sourceWorkspaceId)
382+
await expect(workspaceSelect).toHaveValue(sourceWorkspaceId)
383+
await expect(renameButton).toBeEnabled()
384+
385+
page.once('dialog', async dialog => {
386+
expect(dialog.type()).toBe('prompt')
387+
expect(dialog.defaultValue()).toBe(originalTitle)
388+
await dialog.accept(renamedTitle)
389+
})
390+
391+
await renameButton.click()
392+
await expect(page.locator('#workspaces-status')).toContainText('Renamed workspace.')
393+
394+
const records = await getAllWorkspaceRecords(page)
395+
const renamedRecord = records.find(record => record?.id === sourceWorkspaceId)
396+
397+
expect(renamedRecord).toBeTruthy()
398+
expect(typeof renamedRecord?.prTitle === 'string' ? renamedRecord.prTitle : '').toBe(
399+
renamedTitle,
400+
)
401+
})
402+
316403
test('BYOT controls render with default app entry', async ({ page }) => {
317404
await waitForAppReady(page, appEntryPath)
318405

playwright/github-pr-drawer/open-pr-create.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,7 @@ test('Workspaces repository selector filters contexts and keeps local-only conte
934934
await selectWorkspacesRepositoryFilter(page, '__local__')
935935
const localLabels = await getLocalContextOptionLabels(page)
936936
expect(localLabels).toContain('Select a stored workspace')
937-
expect(localLabels).toContain('local:Alpha local context')
937+
expect(localLabels).toContain('Alpha local context')
938938
expect(localLabels).not.toContain('Alpha active context')
939939
})
940940

@@ -1312,7 +1312,7 @@ test('Switching Workspaces repository scope to Local keeps inactive record repo
13121312
await expect
13131313
.poll(async () => {
13141314
const localLabels = await getLocalContextOptionLabels(page)
1315-
return localLabels.includes('local:feat/component-v8zw')
1315+
return localLabels.includes('feat/component-v8zw')
13161316
})
13171317
.toBe(true)
13181318

src/app.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ const workspacesInitialize = document.getElementById('workspaces-initialize')
150150
const workspacesNew = document.getElementById('workspaces-new')
151151
const workspacesSelect = document.getElementById('workspaces-select')
152152
const workspacesOpen = document.getElementById('workspaces-open')
153+
const workspacesRename = document.getElementById('workspaces-rename')
153154
const workspacesRemove = document.getElementById('workspaces-remove')
154155
const componentPrSyncIcon = document.getElementById('component-pr-sync-icon')
155156
const componentPrSyncIconPath = document.getElementById('component-pr-sync-icon-path')
@@ -425,6 +426,8 @@ let workspacePrContextState = 'inactive'
425426
let workspacePrNumber = null
426427
let workspaceRepositoryFullName = ''
427428
let workspaceScopeMarker = 'local'
429+
let activeWorkspacePersistedPrTitle = ''
430+
let activeWorkspacePersistedHeadBranch = ''
428431
let hasObservedActivePrContextInSession = false
429432
let workspaceContextStatusController = {
430433
render: () => {},
@@ -450,6 +453,8 @@ const toPullRequestNumber = value => {
450453
const setActiveWorkspaceRecordId = nextValue => {
451454
activeWorkspaceRecordId = toNonEmptyWorkspaceText(nextValue)
452455
if (!activeWorkspaceRecordId) {
456+
activeWorkspacePersistedPrTitle = ''
457+
activeWorkspacePersistedHeadBranch = ''
453458
workspaceRepositoryFullName = ''
454459
workspaceScopeMarker = 'local'
455460
}
@@ -607,6 +612,8 @@ workspaceContextStatusController = createWorkspaceContextStatusController({
607612
toNonEmptyWorkspaceText,
608613
getWorkspacePrTitle: () => githubPrTitle?.value,
609614
getWorkspaceHeadBranch: () => githubPrHeadBranch?.value,
615+
getActiveWorkspacePersistedPrTitle: () => activeWorkspacePersistedPrTitle,
616+
getActiveWorkspacePersistedHeadBranch: () => activeWorkspacePersistedHeadBranch,
610617
getWorkspaceScopeMarker: () => workspaceScopeMarker,
611618
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
612619
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
@@ -787,6 +794,15 @@ const onWorkspaceRecordApplied = createWorkspaceRecordAppliedHandler({
787794
getStyleModeValue: () => styleMode.value,
788795
})
789796

797+
const onWorkspaceRecordAppliedWithStatusMetadata = workspace => {
798+
if (workspace && typeof workspace === 'object') {
799+
activeWorkspacePersistedPrTitle = toNonEmptyWorkspaceText(workspace.prTitle)
800+
activeWorkspacePersistedHeadBranch = toNonEmptyWorkspaceText(workspace.head)
801+
}
802+
803+
onWorkspaceRecordApplied(workspace)
804+
}
805+
790806
const {
791807
workspaceSaveController,
792808
listLocalContextRecords,
@@ -878,7 +894,7 @@ const {
878894
getWorkspaceTabByKind,
879895
makeUniqueTabPath,
880896
createWorkspaceTabId,
881-
onWorkspaceRecordApplied,
897+
onWorkspaceRecordApplied: onWorkspaceRecordAppliedWithStatusMetadata,
882898
})
883899

884900
const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
@@ -1116,6 +1132,7 @@ const githubWorkflows = createGitHubWorkflowsSetup({
11161132
workspacesNew,
11171133
workspacesSelect,
11181134
workspacesOpen,
1135+
workspacesRename,
11191136
workspacesRemove,
11201137
},
11211138
workspace: {

src/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,6 @@ <h1>
350350
<div
351351
class="app-grid-workspace-context-status"
352352
id="workspace-context-status"
353-
hidden
354353
role="status"
355354
aria-live="polite"
356355
aria-atomic="true"
@@ -803,6 +802,9 @@ <h2 id="workspaces-title">Workspaces</h2>
803802
<button class="render-button" id="workspaces-open" type="button" disabled>
804803
Open
805804
</button>
805+
<button class="render-button" id="workspaces-rename" type="button" disabled>
806+
Rename
807+
</button>
806808
<button class="render-button" id="workspaces-remove" type="button" disabled>
807809
Remove
808810
</button>

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const initializeGitHubWorkflows = ({
4646
workspacesNew,
4747
workspacesSelect,
4848
workspacesOpen,
49+
workspacesRename,
4950
workspacesRemove,
5051
workspaceStorage,
5152
getActiveWorkspaceRecordId,
@@ -121,6 +122,8 @@ const initializeGitHubWorkflows = ({
121122
const toSafeRepositoryFullName = value =>
122123
typeof value === 'string' ? value.trim() : ''
123124

125+
const toSafeWorkspaceText = value => (typeof value === 'string' ? value.trim() : '')
126+
124127
const shouldApplyActivePrEditorSync = ({ repository, activeContext }) => {
125128
const syncedContextKey = getActivePrContextSyncKey(activeContext)
126129
const currentSyncKey = getActivePrEditorSyncKey()
@@ -425,6 +428,7 @@ const initializeGitHubWorkflows = ({
425428
newButton: workspacesNew,
426429
selectInput: workspacesSelect,
427430
openButton: workspacesOpen,
431+
renameButton: workspacesRename,
428432
removeButton: workspacesRemove,
429433
getRepositoryFilterOptions: () =>
430434
getCurrentWritableRepositories().map(repository => ({
@@ -527,6 +531,81 @@ const initializeGitHubWorkflows = ({
527531
return false
528532
}
529533
},
534+
onRenameSelected: async workspaceId => {
535+
try {
536+
const record = await workspaceStorage.getWorkspaceById(workspaceId)
537+
if (!record) {
538+
await refreshLocalContextOptions()
539+
workspacesDrawerController?.setStatus(
540+
'Stored workspace no longer exists.',
541+
'error',
542+
)
543+
return false
544+
}
545+
546+
const workspaceScope = toSafeWorkspaceText(record.workspaceScope).toLowerCase()
547+
const isLocalWorkspace =
548+
workspaceScope === 'local' ||
549+
(!workspaceScope && !toSafeWorkspaceText(record.repo))
550+
if (!isLocalWorkspace) {
551+
workspacesDrawerController?.setStatus(
552+
'Only Local workspaces can be renamed here.',
553+
'error',
554+
)
555+
return false
556+
}
557+
558+
if (getActiveWorkspaceRecordId() === workspaceId) {
559+
workspacesDrawerController?.setStatus(
560+
'Open a different workspace before renaming this one.',
561+
'error',
562+
)
563+
return false
564+
}
565+
566+
const currentWorkspaceName =
567+
toSafeWorkspaceText(record.prTitle) ||
568+
toSafeWorkspaceText(record.head) ||
569+
toSafeWorkspaceText(record.id) ||
570+
'workspace'
571+
const promptedWorkspaceName = window.prompt(
572+
'Rename local workspace',
573+
currentWorkspaceName,
574+
)
575+
576+
if (promptedWorkspaceName === null) {
577+
return false
578+
}
579+
580+
const nextWorkspaceName = promptedWorkspaceName.trim()
581+
if (!nextWorkspaceName) {
582+
workspacesDrawerController?.setStatus(
583+
'Workspace name cannot be empty.',
584+
'error',
585+
)
586+
return false
587+
}
588+
589+
if (nextWorkspaceName === currentWorkspaceName) {
590+
return false
591+
}
592+
593+
await workspaceStorage.upsertWorkspace({
594+
...record,
595+
prTitle: nextWorkspaceName,
596+
lastModified: Date.now(),
597+
})
598+
599+
await refreshLocalContextOptions()
600+
return true
601+
} catch {
602+
workspacesDrawerController?.setStatus(
603+
'Could not rename stored workspace.',
604+
'error',
605+
)
606+
return false
607+
}
608+
},
530609
onRemoveSelected: async workspaceId => {
531610
confirmAction({
532611
title: 'Remove stored workspace?',

0 commit comments

Comments
 (0)