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
91 changes: 89 additions & 2 deletions playwright/github-byot-ai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,103 @@ 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 }) => {
test('workspace context status stays visible without PAT and after PAT connect', async ({
page,
}) => {
await waitForAppReady(page)

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

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

test('Local workspace can be renamed from Workspaces drawer', async ({ page }) => {
const sourceWorkspaceId = 'local_workspace_rename_source'
const targetWorkspaceId = 'local_workspace_rename_target'
const originalTitle = 'Local rename original title'
const renamedTitle = 'Local rename updated title'

await waitForAppReady(page)

await seedLocalWorkspaceContexts(page, [
{
id: sourceWorkspaceId,
repo: '',
workspaceScope: 'local',
head: 'feat/local-rename-source',
prTitle: originalTitle,
prContextState: 'inactive',
tabs: [
{
id: 'component',
path: 'src/component.tsx',
language: 'tsx',
role: 'component',
content: 'export const App = () => <main>rename source</main>',
order: 0,
source: 'workspace',
dirty: false,
},
],
activeTabId: 'component',
},
{
id: targetWorkspaceId,
repo: '',
workspaceScope: 'local',
head: 'feat/local-rename-target',
prTitle: 'Local rename target title',
prContextState: 'inactive',
tabs: [
{
id: 'component',
path: 'src/component.tsx',
language: 'tsx',
role: 'component',
content: 'export const App = () => <main>rename target</main>',
order: 0,
source: 'workspace',
dirty: false,
},
],
activeTabId: 'component',
},
])

const workspacesToggle = page.getByRole('button', {
name: 'Workspaces',
exact: true,
})
await workspacesToggle.click()

const workspaceSelect = page.getByLabel('Stored workspace')
const renameButton = page.getByRole('button', { name: 'Rename', exact: true })

await workspaceSelect.selectOption(sourceWorkspaceId)
await expect(workspaceSelect).toHaveValue(sourceWorkspaceId)
await expect(renameButton).toBeEnabled()

page.once('dialog', async dialog => {
expect(dialog.type()).toBe('prompt')
expect(dialog.defaultValue()).toBe(originalTitle)
await dialog.accept(renamedTitle)
})

await renameButton.click()
await expect(page.locator('#workspaces-status')).toContainText('Renamed workspace.')

const records = await getAllWorkspaceRecords(page)
const renamedRecord = records.find(record => record?.id === sourceWorkspaceId)

expect(renamedRecord).toBeTruthy()
expect(typeof renamedRecord?.prTitle === 'string' ? renamedRecord.prTitle : '').toBe(
renamedTitle,
)
})

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

Expand Down
4 changes: 2 additions & 2 deletions playwright/github-pr-drawer/open-pr-create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ test('Workspaces repository selector filters contexts and keeps local-only conte
await selectWorkspacesRepositoryFilter(page, '__local__')
const localLabels = await getLocalContextOptionLabels(page)
expect(localLabels).toContain('Select a stored workspace')
expect(localLabels).toContain('local:Alpha local context')
expect(localLabels).toContain('Alpha local context')
expect(localLabels).not.toContain('Alpha active context')
})

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

Expand Down
19 changes: 18 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ const workspacesInitialize = document.getElementById('workspaces-initialize')
const workspacesNew = document.getElementById('workspaces-new')
const workspacesSelect = document.getElementById('workspaces-select')
const workspacesOpen = document.getElementById('workspaces-open')
const workspacesRename = document.getElementById('workspaces-rename')
const workspacesRemove = document.getElementById('workspaces-remove')
const componentPrSyncIcon = document.getElementById('component-pr-sync-icon')
const componentPrSyncIconPath = document.getElementById('component-pr-sync-icon-path')
Expand Down Expand Up @@ -425,6 +426,8 @@ let workspacePrContextState = 'inactive'
let workspacePrNumber = null
let workspaceRepositoryFullName = ''
let workspaceScopeMarker = 'local'
let activeWorkspacePersistedPrTitle = ''
let activeWorkspacePersistedHeadBranch = ''
let hasObservedActivePrContextInSession = false
let workspaceContextStatusController = {
render: () => {},
Expand All @@ -450,6 +453,8 @@ const toPullRequestNumber = value => {
const setActiveWorkspaceRecordId = nextValue => {
activeWorkspaceRecordId = toNonEmptyWorkspaceText(nextValue)
if (!activeWorkspaceRecordId) {
activeWorkspacePersistedPrTitle = ''
activeWorkspacePersistedHeadBranch = ''
workspaceRepositoryFullName = ''
workspaceScopeMarker = 'local'
}
Expand Down Expand Up @@ -607,6 +612,8 @@ workspaceContextStatusController = createWorkspaceContextStatusController({
toNonEmptyWorkspaceText,
getWorkspacePrTitle: () => githubPrTitle?.value,
getWorkspaceHeadBranch: () => githubPrHeadBranch?.value,
getActiveWorkspacePersistedPrTitle: () => activeWorkspacePersistedPrTitle,
getActiveWorkspacePersistedHeadBranch: () => activeWorkspacePersistedHeadBranch,
getWorkspaceScopeMarker: () => workspaceScopeMarker,
getActiveWorkspaceRecordId: () => activeWorkspaceRecordId,
getWorkspaceRepositoryFullName: () => workspaceRepositoryFullName,
Expand Down Expand Up @@ -787,6 +794,15 @@ const onWorkspaceRecordApplied = createWorkspaceRecordAppliedHandler({
getStyleModeValue: () => styleMode.value,
})

const onWorkspaceRecordAppliedWithStatusMetadata = workspace => {
if (workspace && typeof workspace === 'object') {
activeWorkspacePersistedPrTitle = toNonEmptyWorkspaceText(workspace.prTitle)
activeWorkspacePersistedHeadBranch = toNonEmptyWorkspaceText(workspace.head)
}

onWorkspaceRecordApplied(workspace)
}

const {
workspaceSaveController,
listLocalContextRecords,
Expand Down Expand Up @@ -878,7 +894,7 @@ const {
getWorkspaceTabByKind,
makeUniqueTabPath,
createWorkspaceTabId,
onWorkspaceRecordApplied,
onWorkspaceRecordApplied: onWorkspaceRecordAppliedWithStatusMetadata,
})

const { syncActiveWorkspaceRepositoryScope, forkWorkspaceFromCurrentState } =
Expand Down Expand Up @@ -1116,6 +1132,7 @@ const githubWorkflows = createGitHubWorkflowsSetup({
workspacesNew,
workspacesSelect,
workspacesOpen,
workspacesRename,
workspacesRemove,
},
workspace: {
Expand Down
4 changes: 3 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,6 @@ <h1>
<div
class="app-grid-workspace-context-status"
id="workspace-context-status"
hidden
role="status"
aria-live="polite"
aria-atomic="true"
Expand Down Expand Up @@ -803,6 +802,9 @@ <h2 id="workspaces-title">Workspaces</h2>
<button class="render-button" id="workspaces-open" type="button" disabled>
Open
</button>
<button class="render-button" id="workspaces-rename" type="button" disabled>
Rename
</button>
<button class="render-button" id="workspaces-remove" type="button" disabled>
Remove
</button>
Expand Down
79 changes: 79 additions & 0 deletions src/modules/app-core/github-workflows.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const initializeGitHubWorkflows = ({
workspacesNew,
workspacesSelect,
workspacesOpen,
workspacesRename,
workspacesRemove,
workspaceStorage,
getActiveWorkspaceRecordId,
Expand Down Expand Up @@ -121,6 +122,8 @@ const initializeGitHubWorkflows = ({
const toSafeRepositoryFullName = value =>
typeof value === 'string' ? value.trim() : ''

const toSafeWorkspaceText = value => (typeof value === 'string' ? value.trim() : '')

const shouldApplyActivePrEditorSync = ({ repository, activeContext }) => {
const syncedContextKey = getActivePrContextSyncKey(activeContext)
const currentSyncKey = getActivePrEditorSyncKey()
Expand Down Expand Up @@ -425,6 +428,7 @@ const initializeGitHubWorkflows = ({
newButton: workspacesNew,
selectInput: workspacesSelect,
openButton: workspacesOpen,
renameButton: workspacesRename,
removeButton: workspacesRemove,
getRepositoryFilterOptions: () =>
getCurrentWritableRepositories().map(repository => ({
Expand Down Expand Up @@ -527,6 +531,81 @@ const initializeGitHubWorkflows = ({
return false
}
},
onRenameSelected: async workspaceId => {
try {
const record = await workspaceStorage.getWorkspaceById(workspaceId)
if (!record) {
await refreshLocalContextOptions()
workspacesDrawerController?.setStatus(
'Stored workspace no longer exists.',
'error',
)
return false
}

const workspaceScope = toSafeWorkspaceText(record.workspaceScope).toLowerCase()
const isLocalWorkspace =
workspaceScope === 'local' ||
(!workspaceScope && !toSafeWorkspaceText(record.repo))
if (!isLocalWorkspace) {
workspacesDrawerController?.setStatus(
'Only Local workspaces can be renamed here.',
Comment thread
knightedcodemonkey marked this conversation as resolved.
'error',
)
return false
}

if (getActiveWorkspaceRecordId() === workspaceId) {
workspacesDrawerController?.setStatus(
'Open a different workspace before renaming this one.',
'error',
)
return false
}

const currentWorkspaceName =
toSafeWorkspaceText(record.prTitle) ||
toSafeWorkspaceText(record.head) ||
toSafeWorkspaceText(record.id) ||
'workspace'
const promptedWorkspaceName = window.prompt(
'Rename local workspace',
currentWorkspaceName,
)

if (promptedWorkspaceName === null) {
return false
}

const nextWorkspaceName = promptedWorkspaceName.trim()
if (!nextWorkspaceName) {
workspacesDrawerController?.setStatus(
'Workspace name cannot be empty.',
'error',
)
return false
}

if (nextWorkspaceName === currentWorkspaceName) {
return false
}

await workspaceStorage.upsertWorkspace({
...record,
prTitle: nextWorkspaceName,
lastModified: Date.now(),
})

await refreshLocalContextOptions()
return true
} catch {
workspacesDrawerController?.setStatus(
'Could not rename stored workspace.',
'error',
)
return false
}
},
onRemoveSelected: async workspaceId => {
confirmAction({
title: 'Remove stored workspace?',
Expand Down
Loading
Loading