Skip to content
Open
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
23 changes: 5 additions & 18 deletions packages/app/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
dropdownMenuTriggerSelector,
dropdownMenuContentSelector,
projectMenuTriggerSelector,
projectWorkspacesToggleSelector,
titlebarRightSelector,
popoverBodySelector,
listItemSelector,
Expand Down Expand Up @@ -544,26 +543,14 @@ export async function openProjectMenu(page: Page, projectSlug: string) {
}

export async function setWorkspacesEnabled(page: Page, projectSlug: string, enabled: boolean) {
const current = await page
.getByRole("button", { name: "New workspace" })
.first()
.isVisible()
.then((x) => x)
.catch(() => false)

if (current === enabled) return

await openProjectMenu(page, projectSlug)

const toggle = page.locator(projectWorkspacesToggleSelector(projectSlug)).first()
await expect(toggle).toBeVisible()
await toggle.click({ force: true })

const expected = enabled ? "New workspace" : "New session"
await expect(page.getByRole("button", { name: expected }).first()).toBeVisible()
if (!enabled) return
await openSidebar(page)
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
}

export async function openWorkspaceMenu(page: Page, workspaceSlug: string) {
await page.goto(`/${workspaceSlug}/session`)
await openSidebar(page)
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
await expect(item).toBeVisible()
await item.hover()
Expand Down
24 changes: 13 additions & 11 deletions packages/app/e2e/projects/projects-switch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
setWorkspacesEnabled,
sessionIDFromUrl,
} from "../actions"
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { projectSwitchSelector, promptSelector } from "../selectors"
import { createSdk, dirSlug } from "../utils"

function slugFromUrl(url: string) {
Expand Down Expand Up @@ -80,16 +80,18 @@ test("switching back to a project opens the latest workspace session", async ({

const workspaceSlug = slugFromUrl(page.url())
workspaceDir = base64Decode(workspaceSlug)
await openSidebar(page)

const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
await expect(workspace).toBeVisible()
await workspace.hover()

const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first()
await expect(newSession).toBeVisible()
await newSession.click({ force: true })

const rootSdk = createSdk(directory)
await expect
.poll(async () => {
const list = await rootSdk.worktree
.list()
.then((x) => x.data ?? [])
.catch(() => [] as string[])
return workspaceDir ? list.includes(workspaceDir) : false
})
.toBe(true)

await page.goto(`/${workspaceSlug}/session`)
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))

const prompt = page.locator(promptSelector)
Expand Down
53 changes: 26 additions & 27 deletions packages/app/e2e/projects/workspace-new-session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,16 @@ import { base64Decode } from "@opencode-ai/util/encode"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
import { cleanupTestProject, openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { promptSelector } from "../selectors"
import { createSdk } from "../utils"

function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
}

async function waitWorkspaceReady(page: Page, slug: string) {
await openSidebar(page)
await expect
.poll(
async () => {
const item = page.locator(workspaceItemSelector(slug)).first()
try {
await item.hover({ timeout: 500 })
return true
} catch {
return false
}
},
{ timeout: 60_000 },
)
.toBe(true)
async function gotoWorkspace(page: Page, slug: string) {
await page.goto(`/${slug}/session`)
await expect.poll(() => slugFromUrl(page.url()), { timeout: 60_000 }).toBe(slug)
}

async function createWorkspace(page: Page, root: string, seen: string[]) {
Expand All @@ -51,14 +38,9 @@ async function createWorkspace(page: Page, root: string, seen: string[]) {
}

async function openWorkspaceNewSession(page: Page, slug: string) {
await waitWorkspaceReady(page, slug)

const item = page.locator(workspaceItemSelector(slug)).first()
await item.hover()

const button = page.locator(workspaceNewSessionSelector(slug)).first()
await expect(button).toBeVisible()
await button.click({ force: true })
await gotoWorkspace(page, slug)
await openSidebar(page)
await page.getByRole("button", { name: "New session" }).first().click()

await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
await expect(page).toHaveURL(new RegExp(`/${slug}/session(?:[/?#]|$)`))
Expand Down Expand Up @@ -98,6 +80,7 @@ test("new sessions from sidebar workspace actions stay in selected workspace", a
await page.setViewportSize({ width: 1400, height: 800 })

await withProject(async ({ directory, slug: root }) => {
const rootSdk = createSdk(directory)
const workspaces = [] as { slug: string; directory: string }[]
const sessions = [] as string[]

Expand All @@ -107,11 +90,27 @@ test("new sessions from sidebar workspace actions stay in selected workspace", a

const first = await createWorkspace(page, root, [])
workspaces.push(first)
await waitWorkspaceReady(page, first.slug)
await expect
.poll(async () => {
const list = await rootSdk.worktree
.list()
.then((x) => x.data ?? [])
.catch(() => [] as string[])
return list.includes(first.directory)
})
.toBe(true)

const second = await createWorkspace(page, root, [first.slug])
workspaces.push(second)
await waitWorkspaceReady(page, second.slug)
await expect
.poll(async () => {
const list = await rootSdk.worktree
.list()
.then((x) => x.data ?? [])
.catch(() => [] as string[])
return list.includes(second.directory)
})
.toBe(true)

const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
sessions.push(firstSession)
Expand Down
86 changes: 36 additions & 50 deletions packages/app/e2e/projects/workspaces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
clickMenuItem,
confirmDialog,
openSidebar,
openProjectMenu,
openWorkspaceMenu,
setWorkspacesEnabled,
} from "../actions"
Expand All @@ -22,7 +23,7 @@ function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
}

async function setupWorkspaceTest(page: Page, project: { slug: string }) {
async function setupWorkspaceTest(page: Page, project: { slug: string; directory: string }) {
const rootSlug = project.slug
await openSidebar(page)

Expand All @@ -42,42 +43,50 @@ async function setupWorkspaceTest(page: Page, project: { slug: string }) {
const slug = slugFromUrl(page.url())
const dir = base64Decode(slug)

await openSidebar(page)

await expect
.poll(
async () => {
const item = page.locator(workspaceItemSelector(slug)).first()
try {
await item.hover({ timeout: 500 })
return true
} catch {
return false
}
},
{ timeout: 60_000 },
)
.poll(async () => {
const list = await createSdk(project.directory)
.worktree.list()
.then((x) => x.data ?? [])
.catch(() => [] as string[])
return list.includes(dir)
})
.toBe(true)

await page.goto(`/${slug}/session`)
await expect.poll(() => slugFromUrl(page.url()), { timeout: 60_000 }).toBe(slug)
await openSidebar(page)

return { rootSlug, slug, directory: dir }
}

test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
test("workspace actions are available from project menu", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })

await withProject(async ({ slug }) => {
await openSidebar(page)

await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)

await setWorkspacesEnabled(page, slug, true)
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible()

await setWorkspacesEnabled(page, slug, false)
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
const menu = await openProjectMenu(page, slug)
const switchWorkspace = menu.locator('[data-action="project-switch-workspace"]').first()
await expect(switchWorkspace).toBeVisible()
await page.keyboard.press("Escape")

await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const currentSlug = slugFromUrl(page.url())
return currentSlug.length > 0 && currentSlug !== slug
},
{ timeout: 45_000 },
)
.toBe(true)

await openSidebar(page)
const menuAfter = await openProjectMenu(page, slug)
await expect(menuAfter.locator('[data-action="project-switch-workspace"]').first()).toBeEnabled()
})
})

Expand All @@ -86,8 +95,6 @@ test("can create a workspace", async ({ page, withProject }) => {

await withProject(async ({ slug }) => {
await openSidebar(page)
await setWorkspacesEnabled(page, slug, true)

await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()

await page.getByRole("button", { name: "New workspace" }).first().click()
Expand All @@ -104,25 +111,7 @@ test("can create a workspace", async ({ page, withProject }) => {

const workspaceSlug = slugFromUrl(page.url())
const workspaceDir = base64Decode(workspaceSlug)

await openSidebar(page)

await expect
.poll(
async () => {
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
try {
await item.hover({ timeout: 500 })
return true
} catch {
return false
}
},
{ timeout: 60_000 },
)
.toBe(true)

await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))

await cleanupTestProject(workspaceDir)
})
Expand Down Expand Up @@ -160,11 +149,11 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
const menu = page.locator(dropdownMenuContentSelector).first()
await expect(menu).toBeVisible()

const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first()
const toggle = menu.locator('[data-action="project-switch-workspace"]').first()

await expect(toggle).toBeVisible()
await expect(toggle).toBeDisabled()
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
await expect(menu.getByRole("menuitem", { name: "Switch workspace" })).toHaveCount(1)
})
} finally {
await cleanupTestProject(nonGit)
Expand Down Expand Up @@ -297,7 +286,6 @@ test("can delete a workspace", async ({ page, withProject }) => {
await project.gotoSession()

await openSidebar(page)
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
})
})
Expand Down Expand Up @@ -352,8 +340,6 @@ test("can reorder workspaces by drag and drop", async ({ page, withProject }) =>
try {
await openSidebar(page)

await setWorkspacesEnabled(page, rootSlug, true)

for (const _ of [0, 1]) {
const prev = slugFromUrl(page.url())
await page.getByRole("button", { name: "New workspace" }).first().click()
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ export const dict = {
"command.prompt.mode.normal": "Prompt",
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
"command.workspace.toggle": "تبديل مساحات العمل",
"command.workspace.toggle.description": "تمكين أو تعطيل مساحات العمل المتعددة في الشريط الجانبي",
"command.workspace.switch": "تبديل مساحة العمل",
"command.workspace.previous": "مساحة العمل السابقة",
"command.workspace.next": "مساحة العمل التالية",
"command.session.undo": "تراجع",
"command.session.undo.description": "تراجع عن الرسالة الأخيرة",
"command.session.redo": "إعادة",
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/i18n/br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ export const dict = {
"command.prompt.mode.normal": "Prompt",
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
"command.workspace.toggle": "Alternar espaços de trabalho",
"command.workspace.toggle.description": "Habilitar ou desabilitar múltiplos espaços de trabalho na barra lateral",
"command.workspace.switch": "Alternar espaço de trabalho",
"command.workspace.previous": "Espaço de trabalho anterior",
"command.workspace.next": "Próximo espaço de trabalho",
"command.session.undo": "Desfazer",
"command.session.undo.description": "Desfazer a última mensagem",
"command.session.redo": "Refazer",
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/i18n/bs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ export const dict = {
"command.prompt.mode.normal": "Prompt",
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
"command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci",
"command.workspace.switch": "Promijeni radni prostor",
"command.workspace.previous": "Prethodni radni prostor",
"command.workspace.next": "Sljedeći radni prostor",
"command.session.undo": "Poništi",
"command.session.undo.description": "Poništi posljednju poruku",
"command.session.redo": "Vrati",
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/i18n/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ export const dict = {
"command.prompt.mode.normal": "Prompt",
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
"command.workspace.toggle": "Skift arbejdsområder",
"command.workspace.toggle.description": "Aktiver eller deaktiver flere arbejdsområder i sidebjælken",
"command.workspace.switch": "Skift arbejdsområde",
"command.workspace.previous": "Forrige arbejdsområde",
"command.workspace.next": "Næste arbejdsområde",
"command.session.undo": "Fortryd",
"command.session.undo.description": "Fortryd den sidste besked",
"command.session.redo": "Omgør",
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ export const dict = {
"command.prompt.mode.normal": "Prompt",
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
"command.workspace.toggle": "Arbeitsbereiche umschalten",
"command.workspace.toggle.description": "Mehrere Arbeitsbereiche in der Seitenleiste aktivieren oder deaktivieren",
"command.workspace.switch": "Arbeitsbereich wechseln",
"command.workspace.previous": "Vorheriger Arbeitsbereich",
"command.workspace.next": "Nächster Arbeitsbereich",
"command.session.undo": "Rückgängig",
"command.session.undo.description": "Letzte Nachricht rückgängig machen",
"command.session.redo": "Wiederherstellen",
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ export const dict = {
"command.prompt.mode.normal": "Prompt",
"command.permissions.autoaccept.enable": "Auto-accept edits",
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
"command.workspace.toggle": "Toggle workspaces",
"command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar",
"command.workspace.switch": "Switch workspace",
"command.workspace.previous": "Previous workspace",
"command.workspace.next": "Next workspace",
"command.session.undo": "Undo",
"command.session.undo.description": "Undo the last message",
"command.session.redo": "Redo",
Expand Down
Loading
Loading