feat(app): align workspace flows with Q3 designs

This commit is contained in:
LukeParkerDev 2026-08-05 17:56:51 +10:00
parent 7d6b8105f1
commit e98b90fc51
31 changed files with 268 additions and 309 deletions

View file

@ -82,25 +82,18 @@ test("selects local, new, and existing workspaces from the ready-ish start menu"
await trigger.hover()
await expect(page.getByRole("tooltip")).toContainText("Select where to run session")
await trigger.click()
await expect(page.getByRole("menuitem", { name: /Local repository/ })).toContainText("Use current checkout")
await expect(page.getByRole("menuitem", { name: /Local repository/ })).toBeVisible()
const newWorkspace = page.getByRole("menuitem", { name: /New workspace/ })
await expect(newWorkspace).toContainText("Work in parallel")
await expect(page.getByRole("menuitem", { name: /Workspace/ })).toContainText("Pick from existing")
await expect(newWorkspace).toBeVisible()
await expect(page.getByRole("menuitem", { name: /Workspace/ })).toBeVisible()
await expect(page.getByRole("menuitem", { name: "View all" })).toBeVisible()
await newWorkspace.hover()
await expect(page.getByText("Creates an isolated copy from your current checkout", { exact: true })).toBeVisible({
timeout: 3_000,
})
await page.keyboard.press("Escape")
await expect(page.getByText("Creates an isolated copy from your current checkout", { exact: true })).toHaveCount(0)
await newWorkspace.click()
await expect(page.getByRole("button", { name: /New workspace/ })).toBeVisible()
await expect(page.getByText("from main", { exact: true })).toBeVisible()
await page.getByRole("button", { name: /New workspace/ }).click()
await page.getByRole("menuitem", { name: /Workspace/ }).hover()
await expect(page.getByText("Reuse an existing isolated workspace", { exact: true })).toBeVisible()
await page.getByRole("menuitem", { name: "feature" }).click()
await expect(page.getByRole("button", { name: /feature/ })).toBeVisible()
})
@ -126,7 +119,7 @@ test("searches long workspace lists within the available viewport", async ({ pag
const submenu = page
.locator('[data-component="menu-v2-content"]')
.filter({ hasText: "Reuse an existing isolated workspace" })
.filter({ has: page.getByPlaceholder("Search workspaces") })
const search = submenu.getByPlaceholder("Search workspaces")
await expect(search).toBeFocused()
const box = await submenu.boundingBox()

View file

@ -1,4 +1,4 @@
import { createMemo, createSignal, For, onCleanup, Show } from "solid-js"
import { createMemo, createSignal, For, Show } from "solid-js"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
@ -10,14 +10,13 @@ export function PromptWorkspaceSelector(props: {
projectRoot: string
workspaces: string[]
branch?: string
onboarding?: boolean
onChange: (value: string) => void
onDone: () => void
onViewAll: () => void
}) {
const language = useLanguage()
const [search, setSearch] = createSignal("")
const [explanation, setExplanation] = createSignal<"local" | "new">()
let explanationTimer: ReturnType<typeof setTimeout> | undefined
let searchInput: HTMLInputElement | undefined
let focusSearch = false
let pending: { type: "select"; value: string } | { type: "viewAll" } | undefined
@ -35,21 +34,7 @@ export function PromptWorkspaceSelector(props: {
const select = (value: string) => {
pending = { type: "select", value }
}
const hideExplanation = () => {
if (explanationTimer) clearTimeout(explanationTimer)
explanationTimer = undefined
setExplanation()
}
const showExplanation = (value: "local" | "new") => {
hideExplanation()
explanationTimer = setTimeout(() => {
explanationTimer = undefined
setExplanation(value)
}, 800)
}
onCleanup(hideExplanation)
const onOpenChange = (open: boolean) => {
hideExplanation()
if (open) {
setSearch("")
return
@ -85,65 +70,52 @@ export function PromptWorkspaceSelector(props: {
>
<Icon name={icon()} class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 truncate">{label()}</span>
<Show when={props.onboarding}>
<span aria-hidden="true" class="size-1.5 shrink-0 rounded-full bg-v2-text-text-accent" />
</Show>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class="w-[220px]">
<MenuV2.Content class="w-[200px]">
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("session.new.workspace.runIn")}</MenuV2.GroupLabel>
<MenuV2.Item
class="!h-11"
aria-description={language.t("session.new.workspace.local.tooltip")}
onPointerEnter={() => showExplanation("local")}
onPointerLeave={hideExplanation}
onFocusIn={() => showExplanation("local")}
onFocusOut={hideExplanation}
onKeyDown={(event: KeyboardEvent) => {
if (event.key === "Escape") hideExplanation()
}}
onSelect={() => select("main")}
>
<MenuV2.Item onSelect={() => select("main")}>
<Icon name="monitor" />
<TooltipV2
placement="right"
forceOpen={explanation() === "local"}
value={language.t("session.new.workspace.local.tooltip")}
openDelay={800}
value={
<span class="flex flex-col gap-0.5">
<span>{language.t("session.new.workspace.local")}</span>
<span class="font-[440] text-v2-text-text-muted">
{language.t("session.new.workspace.local.tooltip")}
</span>
</span>
}
class="min-w-0 flex-1"
contentClass="max-w-[min(240px,calc(100vw-16px))] whitespace-normal break-words"
>
<WorkspaceMenuCopy
label={language.t("session.new.workspace.local")}
description={language.t("session.new.workspace.local.description")}
/>
<span class="min-w-0 truncate">{language.t("session.new.workspace.local")}</span>
</TooltipV2>
<Show when={selected() === "main"}>
<Icon name="check" size="small" class="shrink-0" />
</Show>
</MenuV2.Item>
<MenuV2.Item
class="!h-11"
aria-description={language.t("session.new.workspace.new.tooltip")}
onPointerEnter={() => showExplanation("new")}
onPointerLeave={hideExplanation}
onFocusIn={() => showExplanation("new")}
onFocusOut={hideExplanation}
onKeyDown={(event: KeyboardEvent) => {
if (event.key === "Escape") hideExplanation()
}}
onSelect={() => select("create")}
>
<MenuV2.Item onSelect={() => select("create")}>
<Icon name="workspace-new" />
<TooltipV2
placement="right"
forceOpen={explanation() === "new"}
value={language.t("session.new.workspace.new.tooltip")}
openDelay={800}
value={
<span class="flex flex-col gap-0.5">
<span>{language.t("workspace.new")}</span>
<span class="font-[440] text-v2-text-text-muted">
{language.t("session.new.workspace.new.tooltip")}
</span>
</span>
}
class="min-w-0 flex-1"
contentClass="max-w-[min(240px,calc(100vw-16px))] whitespace-normal break-words"
>
<WorkspaceMenuCopy
label={language.t("workspace.new")}
description={language.t("session.new.workspace.new.description")}
/>
<span class="min-w-0 truncate">{language.t("workspace.new")}</span>
</TooltipV2>
<Show when={selected() === "create"}>
<Icon name="check" size="small" class="shrink-0" />
@ -167,7 +139,6 @@ export function PromptWorkspaceSelector(props: {
}}
>
<MenuV2.SubTrigger
class="!h-11"
onKeyDown={(event) => {
if (
event.key === "ArrowRight" ||
@ -179,16 +150,12 @@ export function PromptWorkspaceSelector(props: {
}}
>
<Icon name="workspace-isolated" />
<WorkspaceMenuCopy
label={language.t("session.new.workspace.existing").replace(/…$/, "")}
description={language.t("session.new.workspace.existing.description")}
/>
<span class="min-w-0 flex-1 truncate">
{language.t("session.new.workspace.existing").replace(/…$/, "")}
</span>
</MenuV2.SubTrigger>
<MenuV2.Portal>
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[220px] overflow-y-auto">
<div class="flex min-h-11 items-center px-3 py-2 text-[13px] font-[440] leading-4 tracking-[-0.04px] text-v2-text-text-faint">
{language.t("session.new.workspace.existing.tooltip")}
</div>
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
<Show when={props.workspaces.length >= 10}>
<div class="flex h-7 items-center gap-2 rounded-sm pl-3 pr-2 text-v2-icon-icon-muted">
<Icon name="magnifying-glass" size="small" class="shrink-0" />
@ -281,12 +248,3 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?
</Show>
)
}
function WorkspaceMenuCopy(props: { label: string; description: string }) {
return (
<span class="flex min-w-0 flex-1 flex-col gap-1">
<span class="truncate">{props.label}</span>
<span class="truncate text-v2-text-text-muted">{props.description}</span>
</span>
)
}

View file

@ -12,6 +12,7 @@ import { Worktree } from "@/utils/worktree"
import { WorkspaceOperation } from "@/utils/workspace-operation"
import { showToast } from "@/utils/toast"
import type { ServerScope } from "@/utils/server-scope"
import { workspaceDirectories } from "@/utils/workspace"
import {
WORKSPACE_PLACEMENT_REFRESH_TIMEOUT_MS,
WORKSPACE_PREPARATION_TIMEOUT_MS,
@ -38,7 +39,7 @@ export function SessionWorkspaceMenu(props: {
const blocked = () =>
props.eligible === false || operationPending() || serverSync().session.data.session_working(props.sessionID)
const workspaces = () =>
(props.project.sandboxes ?? []).filter((workspace) => pathKey(workspace) !== pathKey(props.directory))
workspaceDirectories(props.project).filter((workspace) => pathKey(workspace) !== pathKey(props.directory))
const fail = (scope: ServerScope, sessionID: string, message: string) => {
setStore("selected", undefined)
@ -117,7 +118,7 @@ export function SessionWorkspaceMenu(props: {
{props.children}
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content class="w-[180px]">
<MenuV2.Content class="w-[200px]">
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("workspace.move.title")}</MenuV2.GroupLabel>
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move("create")}>
@ -133,7 +134,7 @@ export function SessionWorkspaceMenu(props: {
{language.t("session.new.workspace.existing").replace(/…$/, "")}
</MenuV2.SubTrigger>
<MenuV2.Portal>
<MenuV2.SubContent class="w-[216px]">
<MenuV2.SubContent class="w-[200px]">
<For each={workspaces()}>
{(workspace) => (
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move(workspace)}>

View file

@ -12,7 +12,6 @@ import "./settings-v2.css"
import { SettingsServersV2 } from "./servers"
import { SettingsWorkspacesV2 } from "./workspaces"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLayout } from "@/context/layout"
import { useTabs } from "@/context/tabs"
import { useServerSync } from "@/context/server-sync"
@ -23,18 +22,16 @@ export const DialogSettings: Component<{
const language = useLanguage()
const platform = usePlatform()
const dialog = useDialog()
const layout = useLayout()
const tabs = useTabs()
const serverSync = useServerSync()
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
const directory = createMemo(() => {
const route = layout.route()
if (route.type === "dir-new-sesssion") return route.dir
if (route.type === "draft") {
const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID)
return draft?.type === "draft" ? draft.directory : undefined
const active = tabs.active()
if (active?.type === "draft") return active.directory
if (active?.type === "session") {
const session = serverSync().session.get(active.sessionId)
if (session) return session.directory
}
if (route.type === "session") return serverSync().session.get(route.sessionId)?.directory
return undefined
})

View file

@ -146,7 +146,7 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr
const remove = async (workspace: Workspace, bulk = false, context = captureDeleteContext()) => {
if (!bulk) setStore("deleting", pathKey(workspace.directory))
const preflight = await inspect(workspace, context)
if (preflight.result !== "safe") {
if (preflight.result !== "safe" && (bulk || preflight.result !== "dirty")) {
blocked(preflight.result)
if (!bulk) setStore("deleting", undefined)
return
@ -443,7 +443,9 @@ function DialogDeleteWorkspace(props: {
<>
{language.t("workspace.delete.confirm", { name: getFilename(props.workspace.directory) })}
<br />
{props.workspace.directory}
<code class="max-w-full rounded-[4px] bg-[color-mix(in_oklch,var(--v2-text-text-base)_8%,transparent)] px-1 py-0.5 font-mono text-xs font-medium leading-4 text-v2-text-text-base break-all">
{props.workspace.directory}
</code>
<br />
{language.t("settings.workspaces.delete.warning")}
<br />
@ -459,7 +461,11 @@ function DialogDeleteWorkspace(props: {
<ButtonV2
type="button"
variant="danger"
disabled={status.isPending || status.isError || status.data?.result !== "safe"}
disabled={
status.isPending ||
status.isError ||
(status.data?.result !== "safe" && status.data?.result !== "dirty")
}
onClick={remove}
>
{language.t("workspace.delete.button")}

View file

@ -177,6 +177,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
}
const actions = {
active() {
if (location.pathname === "/") return
const key = recentKey()
return store.find((tab) => tabKey(tab) === key)
},
addSessionTab: (tab: Omit<SessionTab, "type">) => {
const next = { type: "session" as const, ...tab }
const existing = store.find((item) => tabKey(item) === tabKey(next))

View file

@ -896,13 +896,9 @@ export const dict = {
"session.delete.button": "حذف الجلسة",
"workspace.new": "مساحة عمل جديدة",
"common.viewAll": "عرض الكل",
"session.new.workspace.local.tooltip": "استخدام نسخة العمل الحالية",
"session.new.workspace.new.tooltip": "إنشاء نسخة عمل معزولة",
"session.new.workspace.fromBranch": "من {{branch}}",
"session.new.workspace.local.description": "استخدام نسخة العمل الحالية",
"session.new.workspace.new.description": "العمل بالتوازي",
"session.new.workspace.existing.description": "الاختيار من مساحات العمل الموجودة",
"session.new.workspace.local.tooltip": "تعمل الجلسة في مجلد مشروعك",
"session.new.workspace.new.tooltip": "ينشئ نسخة معزولة من نسخة العمل الحالية لديك",
"session.new.workspace.existing.tooltip": "إعادة استخدام مساحة عمل معزولة موجودة",
"session.new.workspace.trigger.tooltip": "تحديد مكان تشغيل الجلسة",
"session.new.workspace.search.placeholder": "البحث في مساحات العمل",
"settings.tab.workspaces": "مساحات العمل",
@ -912,7 +908,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} مساحات عمل",
"settings.workspaces.sessions.one": "{{count}} جلسة في {{project}}",
"settings.workspaces.sessions.other": "{{count}} جلسات في {{project}}",
"settings.workspaces.lastActive": "آخر نشاط: {{time}}",
"settings.workspaces.lastActiveSession": "آخر جلسة نشطة",
"settings.workspaces.deleteAll": "حذف جميع مساحات العمل",
"settings.workspaces.deleteAll.confirm": "حذف جميع مساحات العمل البالغ عددها {{count}}؟",
@ -922,7 +917,6 @@ export const dict = {
"سيتم حذف مساحات العمل المحددة وعددها {{count}} في {{project}} نهائيًا فقط إذا كانت كل واحدة نظيفة وغير نشطة ولا تحتوي على جلسات مرتبطة.",
"settings.workspaces.delete.blocked.active": "لا يمكن حذف مساحة العمل النشطة.",
"settings.workspaces.delete.blocked.linked": "تحتوي مساحة العمل هذه على جلسات مرتبطة ولا يمكن حذفها.",
"settings.workspaces.section.defaults": "الجلسات الجديدة",
"settings.workspaces.default.title": "البيئة الافتراضية",
"settings.workspaces.default.description": "اختر مكان بدء الجلسات الجديدة",
"settings.workspaces.default.lastUsed": "آخر بيئة مستخدمة لكل مشروع",

View file

@ -910,13 +910,9 @@ export const dict = {
"session.delete.button": "Excluir sessão",
"workspace.new": "Novo espaço de trabalho",
"common.viewAll": "Ver todos",
"session.new.workspace.local.tooltip": "Usar o checkout atual",
"session.new.workspace.new.tooltip": "Criar checkout isolado",
"session.new.workspace.fromBranch": "de {{branch}}",
"session.new.workspace.local.description": "Usar o checkout atual",
"session.new.workspace.new.description": "Trabalhar em paralelo",
"session.new.workspace.existing.description": "Escolher um espaço de trabalho existente",
"session.new.workspace.local.tooltip": "A sessão é executada na pasta do seu projeto",
"session.new.workspace.new.tooltip": "Cria uma cópia isolada a partir do seu checkout atual",
"session.new.workspace.existing.tooltip": "Reutilizar um espaço de trabalho isolado existente",
"session.new.workspace.trigger.tooltip": "Selecionar onde executar a sessão",
"session.new.workspace.search.placeholder": "Pesquisar espaços de trabalho",
"settings.tab.workspaces": "Espaços de trabalho",
@ -926,7 +922,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} espaços de trabalho",
"settings.workspaces.sessions.one": "{{count}} sessão em {{project}}",
"settings.workspaces.sessions.other": "{{count}} sessões em {{project}}",
"settings.workspaces.lastActive": "Última atividade: {{time}}",
"settings.workspaces.lastActiveSession": "Última sessão ativa",
"settings.workspaces.deleteAll": "Excluir todos os espaços de trabalho",
"settings.workspaces.deleteAll.confirm": "Excluir todos os {{count}} espaços de trabalho?",
@ -937,7 +932,6 @@ export const dict = {
"settings.workspaces.delete.blocked.active": "O espaço de trabalho ativo não pode ser excluído.",
"settings.workspaces.delete.blocked.linked":
"Este espaço de trabalho tem sessões vinculadas e não pode ser excluído.",
"settings.workspaces.section.defaults": "Novas sessões",
"settings.workspaces.default.title": "Ambiente padrão",
"settings.workspaces.default.description": "Escolha onde as novas sessões começam",
"settings.workspaces.default.lastUsed": "Último usado por projeto",

View file

@ -986,13 +986,9 @@ export const dict = {
"workspace.new": "Novi radni prostor",
"common.viewAll": "Prikaži sve",
"session.new.workspace.local.tooltip": "Koristi trenutni checkout",
"session.new.workspace.new.tooltip": "Kreiraj izolovani checkout",
"session.new.workspace.fromBranch": "iz {{branch}}",
"session.new.workspace.local.description": "Koristi trenutni checkout",
"session.new.workspace.new.description": "Radi paralelno",
"session.new.workspace.existing.description": "Odaberi postojeći radni prostor",
"session.new.workspace.local.tooltip": "Sesija se pokreće u folderu vašeg projekta",
"session.new.workspace.new.tooltip": "Kreira izolovanu kopiju vašeg trenutnog checkouta",
"session.new.workspace.existing.tooltip": "Ponovo koristi postojeći izolovani radni prostor",
"session.new.workspace.trigger.tooltip": "Odaberi gdje pokrenuti sesiju",
"session.new.workspace.search.placeholder": "Pretraži radne prostore",
"settings.tab.workspaces": "Radni prostori",
@ -1002,7 +998,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} radnih prostora",
"settings.workspaces.sessions.one": "{{count}} sesija u projektu {{project}}",
"settings.workspaces.sessions.other": "{{count}} sesija u projektu {{project}}",
"settings.workspaces.lastActive": "Posljednja aktivnost: {{time}}",
"settings.workspaces.lastActiveSession": "Posljednja aktivna sesija",
"settings.workspaces.deleteAll": "Izbriši sve radne prostore",
"settings.workspaces.deleteAll.confirm": "Izbrisati svih {{count}} radnih prostora?",
@ -1012,7 +1007,6 @@ export const dict = {
"Odabranih {{count}} radnih prostora u {{project}} bit će trajno izbrisano samo ako je svaki čist, neaktivan i nema povezanih sesija.",
"settings.workspaces.delete.blocked.active": "Aktivni radni prostor nije moguće izbrisati.",
"settings.workspaces.delete.blocked.linked": "Ovaj radni prostor ima povezane sesije i nije ga moguće izbrisati.",
"settings.workspaces.section.defaults": "Nove sesije",
"settings.workspaces.default.title": "Zadano okruženje",
"settings.workspaces.default.description": "Odaberite gdje počinju nove sesije",
"settings.workspaces.default.lastUsed": "Posljednje korišteno po projektu",

View file

@ -978,13 +978,9 @@ export const dict = {
"workspace.new": "Nyt arbejdsområde",
"common.viewAll": "Vis alle",
"session.new.workspace.local.tooltip": "Brug det aktuelle checkout",
"session.new.workspace.new.tooltip": "Opret et isoleret checkout",
"session.new.workspace.fromBranch": "fra {{branch}}",
"session.new.workspace.local.description": "Brug det aktuelle checkout",
"session.new.workspace.new.description": "Arbejd parallelt",
"session.new.workspace.existing.description": "Vælg blandt eksisterende arbejdsområder",
"session.new.workspace.local.tooltip": "Sessionen kører i din projektmappe",
"session.new.workspace.new.tooltip": "Opretter en isoleret kopi af dit aktuelle checkout",
"session.new.workspace.existing.tooltip": "Genbrug et eksisterende isoleret arbejdsområde",
"session.new.workspace.trigger.tooltip": "Vælg, hvor sessionen skal køre",
"session.new.workspace.search.placeholder": "Søg i arbejdsområder",
"settings.tab.workspaces": "Arbejdsområder",
@ -994,7 +990,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} arbejdsområder",
"settings.workspaces.sessions.one": "{{count}} session i {{project}}",
"settings.workspaces.sessions.other": "{{count}} sessioner i {{project}}",
"settings.workspaces.lastActive": "Senest aktiv: {{time}}",
"settings.workspaces.lastActiveSession": "Senest aktive session",
"settings.workspaces.deleteAll": "Slet alle arbejdsområder",
"settings.workspaces.deleteAll.confirm": "Slet alle {{count}} arbejdsområder?",
@ -1004,7 +999,6 @@ export const dict = {
"De {{count}} valgte arbejdsområder i {{project}} fjernes permanent, men kun hvis hvert område er rent, inaktivt og ikke har tilknyttede sessioner.",
"settings.workspaces.delete.blocked.active": "Det aktive arbejdsområde kan ikke slettes.",
"settings.workspaces.delete.blocked.linked": "Dette arbejdsområde har tilknyttede sessioner og kan ikke slettes.",
"settings.workspaces.section.defaults": "Nye sessioner",
"settings.workspaces.default.title": "Standardmiljø",
"settings.workspaces.default.description": "Vælg, hvor nye sessioner starter",
"settings.workspaces.default.lastUsed": "Senest brugt pr. projekt",

View file

@ -922,13 +922,9 @@ export const dict = {
"session.delete.button": "Sitzung löschen",
"workspace.new": "Neuer Arbeitsbereich",
"common.viewAll": "Alle anzeigen",
"session.new.workspace.local.tooltip": "Aktuellen Checkout verwenden",
"session.new.workspace.new.tooltip": "Isolierten Checkout erstellen",
"session.new.workspace.fromBranch": "aus {{branch}}",
"session.new.workspace.local.description": "Aktuellen Checkout verwenden",
"session.new.workspace.new.description": "Parallel arbeiten",
"session.new.workspace.existing.description": "Vorhandenen Arbeitsbereich auswählen",
"session.new.workspace.local.tooltip": "Die Sitzung läuft in Ihrem Projektverzeichnis",
"session.new.workspace.new.tooltip": "Erstellt eine isolierte Kopie Ihres aktuellen Checkouts",
"session.new.workspace.existing.tooltip": "Vorhandenen isolierten Arbeitsbereich wiederverwenden",
"session.new.workspace.trigger.tooltip": "Auswählen, wo die Sitzung ausgeführt werden soll",
"session.new.workspace.search.placeholder": "Arbeitsbereiche durchsuchen",
"settings.tab.workspaces": "Arbeitsbereiche",
@ -938,7 +934,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} Arbeitsbereiche",
"settings.workspaces.sessions.one": "{{count}} Sitzung in {{project}}",
"settings.workspaces.sessions.other": "{{count}} Sitzungen in {{project}}",
"settings.workspaces.lastActive": "Zuletzt aktiv: {{time}}",
"settings.workspaces.lastActiveSession": "Zuletzt aktive Sitzung",
"settings.workspaces.deleteAll": "Alle Arbeitsbereiche löschen",
"settings.workspaces.deleteAll.confirm": "Alle {{count}} Arbeitsbereiche löschen?",
@ -949,7 +944,6 @@ export const dict = {
"settings.workspaces.delete.blocked.active": "Der aktive Arbeitsbereich kann nicht gelöscht werden.",
"settings.workspaces.delete.blocked.linked":
"Dieser Arbeitsbereich hat verknüpfte Sitzungen und kann nicht gelöscht werden.",
"settings.workspaces.section.defaults": "Neue Sitzungen",
"settings.workspaces.default.title": "Standardumgebung",
"settings.workspaces.default.description": "Wählen Sie, wo neue Sitzungen starten",
"settings.workspaces.default.lastUsed": "Zuletzt pro Projekt verwendet",

View file

@ -1077,13 +1077,9 @@ export const dict = {
"workspace.new": "New workspace",
"common.viewAll": "View all",
"session.new.workspace.local.tooltip": "Use current checkout",
"session.new.workspace.new.tooltip": "Create isolated checkout",
"session.new.workspace.fromBranch": "from {{branch}}",
"session.new.workspace.local.description": "Use current checkout",
"session.new.workspace.new.description": "Work in parallel",
"session.new.workspace.existing.description": "Pick from existing",
"session.new.workspace.local.tooltip": "Session runs in your project folder",
"session.new.workspace.new.tooltip": "Creates an isolated copy from your current checkout",
"session.new.workspace.existing.tooltip": "Reuse an existing isolated workspace",
"session.new.workspace.trigger.tooltip": "Select where to run session",
"session.new.workspace.search.placeholder": "Search workspaces",
"settings.tab.workspaces": "Workspaces",
@ -1093,7 +1089,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} workspaces",
"settings.workspaces.sessions.one": "{{count}} session in {{project}}",
"settings.workspaces.sessions.other": "{{count}} sessions in {{project}}",
"settings.workspaces.lastActive": "Last active {{time}}",
"settings.workspaces.lastActiveSession": "Last active session",
"settings.workspaces.deleteAll": "Delete all workspaces",
"settings.workspaces.deleteAll.confirm": "Delete all {{count}} workspaces?",
@ -1103,7 +1098,6 @@ export const dict = {
"The {{count}} selected workspaces in {{project}} will be permanently removed only if each is clean, inactive, and has no linked sessions.",
"settings.workspaces.delete.blocked.active": "The active workspace cannot be deleted.",
"settings.workspaces.delete.blocked.linked": "This workspace has linked sessions and cannot be deleted.",
"settings.workspaces.section.defaults": "New sessions",
"settings.workspaces.default.title": "Default environment",
"settings.workspaces.default.description": "Choose where new sessions start",
"settings.workspaces.default.lastUsed": "Last used per project",
@ -1113,6 +1107,9 @@ export const dict = {
"workspace.move.failed": "Failed to move session",
"workspace.lifecycle.creating": "Creating workspace",
"workspace.lifecycle.created": "Workspace created",
"workspace.lifecycle.starting": "Starting session",
"workspace.onboarding.title": "Isolate sessions with workspaces",
"workspace.onboarding.description": "Each gets its own checkout, so nothing interferes with your local repository",
"workspace.lifecycle.moving": "Moving to workspace",
"workspace.lifecycle.set": "Workspace set",
"workspace.type.local": "local",

View file

@ -994,13 +994,9 @@ export const dict = {
"workspace.new": "Nuevo espacio de trabajo",
"common.viewAll": "Ver todo",
"session.new.workspace.local.tooltip": "Usar el checkout actual",
"session.new.workspace.new.tooltip": "Crear un checkout aislado",
"session.new.workspace.fromBranch": "desde {{branch}}",
"session.new.workspace.local.description": "Usar el checkout actual",
"session.new.workspace.new.description": "Trabajar en paralelo",
"session.new.workspace.existing.description": "Elegir un espacio de trabajo existente",
"session.new.workspace.local.tooltip": "La sesión se ejecuta en la carpeta de tu proyecto",
"session.new.workspace.new.tooltip": "Crea una copia aislada a partir de tu checkout actual",
"session.new.workspace.existing.tooltip": "Reutilizar un espacio de trabajo aislado existente",
"session.new.workspace.trigger.tooltip": "Seleccionar dónde ejecutar la sesión",
"session.new.workspace.search.placeholder": "Buscar espacios de trabajo",
"settings.tab.workspaces": "Espacios de trabajo",
@ -1010,7 +1006,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} espacios de trabajo",
"settings.workspaces.sessions.one": "{{count}} sesión en {{project}}",
"settings.workspaces.sessions.other": "{{count}} sesiones en {{project}}",
"settings.workspaces.lastActive": "Última actividad: {{time}}",
"settings.workspaces.lastActiveSession": "Última sesión activa",
"settings.workspaces.deleteAll": "Eliminar todos los espacios de trabajo",
"settings.workspaces.deleteAll.confirm": "¿Eliminar los {{count}} espacios de trabajo?",
@ -1021,7 +1016,6 @@ export const dict = {
"settings.workspaces.delete.blocked.active": "El espacio de trabajo activo no se puede eliminar.",
"settings.workspaces.delete.blocked.linked":
"Este espacio de trabajo tiene sesiones vinculadas y no se puede eliminar.",
"settings.workspaces.section.defaults": "Nuevas sesiones",
"settings.workspaces.default.title": "Entorno predeterminado",
"settings.workspaces.default.description": "Elige dónde se inician las nuevas sesiones",
"settings.workspaces.default.lastUsed": "Último usado en cada proyecto",

View file

@ -921,13 +921,9 @@ export const dict = {
"session.delete.button": "Supprimer la session",
"workspace.new": "Nouvel espace de travail",
"common.viewAll": "Tout afficher",
"session.new.workspace.local.tooltip": "Utiliser le checkout actuel",
"session.new.workspace.new.tooltip": "Créer un checkout isolé",
"session.new.workspace.fromBranch": "depuis {{branch}}",
"session.new.workspace.local.description": "Utiliser le checkout actuel",
"session.new.workspace.new.description": "Travailler en parallèle",
"session.new.workspace.existing.description": "Choisir un espace de travail existant",
"session.new.workspace.local.tooltip": "La session s'exécute dans le dossier de votre projet",
"session.new.workspace.new.tooltip": "Crée une copie isolée à partir de votre checkout actuel",
"session.new.workspace.existing.tooltip": "Réutiliser un espace de travail isolé existant",
"session.new.workspace.trigger.tooltip": "Choisir où exécuter la session",
"session.new.workspace.search.placeholder": "Rechercher des espaces de travail",
"settings.tab.workspaces": "Espaces de travail",
@ -937,7 +933,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} espaces de travail",
"settings.workspaces.sessions.one": "{{count}} session dans {{project}}",
"settings.workspaces.sessions.other": "{{count}} sessions dans {{project}}",
"settings.workspaces.lastActive": "Dernière activité : {{time}}",
"settings.workspaces.lastActiveSession": "Dernière session active",
"settings.workspaces.deleteAll": "Supprimer tous les espaces de travail",
"settings.workspaces.deleteAll.confirm": "Supprimer les {{count}} espaces de travail ?",
@ -948,7 +943,6 @@ export const dict = {
"settings.workspaces.delete.blocked.active": "Lespace de travail actif ne peut pas être supprimé.",
"settings.workspaces.delete.blocked.linked":
"Cet espace de travail contient des sessions liées et ne peut pas être supprimé.",
"settings.workspaces.section.defaults": "Nouvelles sessions",
"settings.workspaces.default.title": "Environnement par défaut",
"settings.workspaces.default.description": "Choisissez où démarrent les nouvelles sessions",
"settings.workspaces.default.lastUsed": "Dernier utilisé par projet",

View file

@ -903,13 +903,9 @@ export const dict = {
"session.delete.button": "セッションを削除",
"workspace.new": "新しいワークスペース",
"common.viewAll": "すべて表示",
"session.new.workspace.local.tooltip": "現在のチェックアウトを使用",
"session.new.workspace.new.tooltip": "隔離されたチェックアウトを作成",
"session.new.workspace.fromBranch": "{{branch}} から",
"session.new.workspace.local.description": "現在のチェックアウトを使用",
"session.new.workspace.new.description": "並行して作業",
"session.new.workspace.existing.description": "既存のワークスペースから選択",
"session.new.workspace.local.tooltip": "セッションはプロジェクトフォルダで実行されます",
"session.new.workspace.new.tooltip": "現在のチェックアウトから独立したコピーを作成します",
"session.new.workspace.existing.tooltip": "既存の独立したワークスペースを再利用します",
"session.new.workspace.trigger.tooltip": "セッションの実行先を選択します",
"session.new.workspace.search.placeholder": "ワークスペースを検索",
"settings.tab.workspaces": "ワークスペース",
@ -919,7 +915,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}}件のワークスペース",
"settings.workspaces.sessions.one": "{{project}}のセッション{{count}}件",
"settings.workspaces.sessions.other": "{{project}}のセッション{{count}}件",
"settings.workspaces.lastActive": "最終アクティブ: {{time}}",
"settings.workspaces.lastActiveSession": "最後にアクティブだったセッション",
"settings.workspaces.deleteAll": "すべてのワークスペースを削除",
"settings.workspaces.deleteAll.confirm": "{{count}}件のワークスペースをすべて削除しますか?",
@ -929,7 +924,6 @@ export const dict = {
"{{project}} で選択された{{count}}件のワークスペースは、それぞれがクリーンで非アクティブかつリンクされたセッションがない場合にのみ完全に削除されます。",
"settings.workspaces.delete.blocked.active": "アクティブなワークスペースは削除できません。",
"settings.workspaces.delete.blocked.linked": "このワークスペースにはリンクされたセッションがあるため削除できません。",
"settings.workspaces.section.defaults": "新しいセッション",
"settings.workspaces.default.title": "デフォルト環境",
"settings.workspaces.default.description": "新しいセッションの開始場所を選択します",
"settings.workspaces.default.lastUsed": "プロジェクトごとに前回使用した環境",

View file

@ -744,13 +744,9 @@ export const dict = {
"session.delete.button": "세션 삭제",
"workspace.new": "새 작업 공간",
"common.viewAll": "모두 보기",
"session.new.workspace.local.tooltip": "현재 체크아웃 사용",
"session.new.workspace.new.tooltip": "격리된 체크아웃 생성",
"session.new.workspace.fromBranch": "{{branch}}에서",
"session.new.workspace.local.description": "현재 체크아웃 사용",
"session.new.workspace.new.description": "병렬로 작업",
"session.new.workspace.existing.description": "기존 작업 공간 중에서 선택",
"session.new.workspace.local.tooltip": "세션은 프로젝트 폴더에서 실행됩니다",
"session.new.workspace.new.tooltip": "현재 체크아웃을 기반으로 격리된 복사본을 만듭니다",
"session.new.workspace.existing.tooltip": "기존의 격리된 작업 공간을 재사용합니다",
"session.new.workspace.trigger.tooltip": "세션을 실행할 위치를 선택하세요",
"session.new.workspace.search.placeholder": "작업 공간 검색",
"settings.tab.workspaces": "작업 공간",
@ -760,7 +756,6 @@ export const dict = {
"settings.workspaces.count.other": "작업 공간 {{count}}개",
"settings.workspaces.sessions.one": "{{project}}에 세션 {{count}}개",
"settings.workspaces.sessions.other": "{{project}}에 세션 {{count}}개",
"settings.workspaces.lastActive": "마지막 활동: {{time}}",
"settings.workspaces.lastActiveSession": "마지막 활성 세션",
"settings.workspaces.deleteAll": "모든 작업 공간 삭제",
"settings.workspaces.deleteAll.confirm": "작업 공간 {{count}}개를 모두 삭제할까요?",
@ -770,7 +765,6 @@ export const dict = {
"{{project}}에서 선택한 작업 공간 {{count}}개는 각각 깨끗하고 비활성 상태이며 연결된 세션이 없는 경우에만 영구적으로 삭제됩니다.",
"settings.workspaces.delete.blocked.active": "활성 작업 공간은 삭제할 수 없습니다.",
"settings.workspaces.delete.blocked.linked": "이 작업 공간에는 연결된 세션이 있어 삭제할 수 없습니다.",
"settings.workspaces.section.defaults": "새 세션",
"settings.workspaces.default.title": "기본 환경",
"settings.workspaces.default.description": "새 세션을 시작할 위치를 선택하세요",
"settings.workspaces.default.lastUsed": "프로젝트별 마지막 사용 환경",

View file

@ -831,13 +831,9 @@ export const dict = {
"workspace.new": "Nytt arbeidsområde",
"common.viewAll": "Vis alle",
"session.new.workspace.local.tooltip": "Bruk gjeldende checkout",
"session.new.workspace.new.tooltip": "Opprett isolert checkout",
"session.new.workspace.fromBranch": "fra {{branch}}",
"session.new.workspace.local.description": "Bruk gjeldende checkout",
"session.new.workspace.new.description": "Arbeid parallelt",
"session.new.workspace.existing.description": "Velg et eksisterende arbeidsområde",
"session.new.workspace.local.tooltip": "Sesjonen kjører i prosjektmappen din",
"session.new.workspace.new.tooltip": "Oppretter en isolert kopi fra checkouten du bruker nå",
"session.new.workspace.existing.tooltip": "Gjenbruk et eksisterende isolert arbeidsområde",
"session.new.workspace.trigger.tooltip": "Velg hvor sesjonen skal kjøres",
"session.new.workspace.search.placeholder": "Søk i arbeidsområder",
"settings.tab.workspaces": "Arbeidsområder",
@ -847,7 +843,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} arbeidsområder",
"settings.workspaces.sessions.one": "{{count}} økt i {{project}}",
"settings.workspaces.sessions.other": "{{count}} økter i {{project}}",
"settings.workspaces.lastActive": "Sist aktiv: {{time}}",
"settings.workspaces.lastActiveSession": "Sist aktive økt",
"settings.workspaces.deleteAll": "Slett alle arbeidsområder",
"settings.workspaces.deleteAll.confirm": "Slette alle {{count}} arbeidsområder?",
@ -857,7 +852,6 @@ export const dict = {
"De {{count}} valgte arbeidsområdene i {{project}} fjernes permanent bare hvis hvert område er rent, inaktivt og ikke har tilknyttede økter.",
"settings.workspaces.delete.blocked.active": "Det aktive arbeidsområdet kan ikke slettes.",
"settings.workspaces.delete.blocked.linked": "Dette arbeidsområdet har tilknyttede økter og kan ikke slettes.",
"settings.workspaces.section.defaults": "Nye økter",
"settings.workspaces.default.title": "Standardmiljø",
"settings.workspaces.default.description": "Velg hvor nye økter starter",
"settings.workspaces.default.lastUsed": "Sist brukt per prosjekt",

View file

@ -909,13 +909,9 @@ export const dict = {
"session.delete.button": "Usuń sesję",
"workspace.new": "Nowa przestrzeń robocza",
"common.viewAll": "Pokaż wszystko",
"session.new.workspace.local.tooltip": "Użyj bieżącego checkoutu",
"session.new.workspace.new.tooltip": "Utwórz izolowany checkout",
"session.new.workspace.fromBranch": "z {{branch}}",
"session.new.workspace.local.description": "Użyj bieżącego checkoutu",
"session.new.workspace.new.description": "Pracuj równolegle",
"session.new.workspace.existing.description": "Wybierz istniejącą przestrzeń roboczą",
"session.new.workspace.local.tooltip": "Sesja działa w folderze Twojego projektu",
"session.new.workspace.new.tooltip": "Tworzy izolowaną kopię bieżącego checkoutu",
"session.new.workspace.existing.tooltip": "Użyj ponownie istniejącej izolowanej przestrzeni roboczej",
"session.new.workspace.trigger.tooltip": "Wybierz, gdzie uruchomić sesję",
"session.new.workspace.search.placeholder": "Szukaj przestrzeni roboczych",
"settings.tab.workspaces": "Obszary robocze",
@ -925,7 +921,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} obszarów roboczych",
"settings.workspaces.sessions.one": "{{count}} sesja w {{project}}",
"settings.workspaces.sessions.other": "{{count}} sesji w {{project}}",
"settings.workspaces.lastActive": "Ostatnia aktywność: {{time}}",
"settings.workspaces.lastActiveSession": "Ostatnia aktywna sesja",
"settings.workspaces.deleteAll": "Usuń wszystkie obszary robocze",
"settings.workspaces.deleteAll.confirm": "Usunąć wszystkie obszary robocze ({{count}})?",
@ -935,7 +930,6 @@ export const dict = {
"Wybrane obszary robocze ({{count}}) w {{project}} zostaną trwale usunięte tylko wtedy, gdy każdy jest czysty, nieaktywny i nie ma powiązanych sesji.",
"settings.workspaces.delete.blocked.active": "Nie można usunąć aktywnego obszaru roboczego.",
"settings.workspaces.delete.blocked.linked": "Ten obszar roboczy ma powiązane sesje i nie można go usunąć.",
"settings.workspaces.section.defaults": "Nowe sesje",
"settings.workspaces.default.title": "Domyślne środowisko",
"settings.workspaces.default.description": "Wybierz, gdzie mają się rozpoczynać nowe sesje",
"settings.workspaces.default.lastUsed": "Ostatnio używane w każdym projekcie",

View file

@ -989,13 +989,9 @@ export const dict = {
"workspace.new": "Новое рабочее пространство",
"common.viewAll": "Показать все",
"session.new.workspace.local.tooltip": "Использовать текущую рабочую копию",
"session.new.workspace.new.tooltip": "Создать изолированную копию",
"session.new.workspace.fromBranch": "из {{branch}}",
"session.new.workspace.local.description": "Использовать текущую рабочую копию",
"session.new.workspace.new.description": "Работать параллельно",
"session.new.workspace.existing.description": "Выбрать существующее рабочее пространство",
"session.new.workspace.local.tooltip": "Сессия запускается в папке вашего проекта",
"session.new.workspace.new.tooltip": "Создаёт изолированную копию на основе вашей текущей рабочей копии",
"session.new.workspace.existing.tooltip": "Повторно использовать существующее изолированное рабочее пространство",
"session.new.workspace.trigger.tooltip": "Выбрать, где запустить сессию",
"session.new.workspace.search.placeholder": "Поиск рабочих пространств",
"settings.tab.workspaces": "Рабочие пространства",
@ -1005,7 +1001,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} рабочих пространств",
"settings.workspaces.sessions.one": "{{count}} сессия в {{project}}",
"settings.workspaces.sessions.other": "{{count}} сессий в {{project}}",
"settings.workspaces.lastActive": "Последняя активность: {{time}}",
"settings.workspaces.lastActiveSession": "Последний активный сеанс",
"settings.workspaces.deleteAll": "Удалить все рабочие пространства",
"settings.workspaces.deleteAll.confirm": "Удалить все рабочие пространства ({{count}})?",
@ -1016,7 +1011,6 @@ export const dict = {
"settings.workspaces.delete.blocked.active": "Активное рабочее пространство нельзя удалить.",
"settings.workspaces.delete.blocked.linked":
"У этого рабочего пространства есть связанные сеансы, поэтому его нельзя удалить.",
"settings.workspaces.section.defaults": "Новые сеансы",
"settings.workspaces.default.title": "Среда по умолчанию",
"settings.workspaces.default.description": "Выберите, где будут запускаться новые сеансы",
"settings.workspaces.default.lastUsed": "Последняя использованная среда проекта",

View file

@ -973,13 +973,9 @@ export const dict = {
"workspace.new": "พื้นที่ทำงานใหม่",
"common.viewAll": "ดูทั้งหมด",
"session.new.workspace.local.tooltip": "ใช้ checkout ปัจจุบัน",
"session.new.workspace.new.tooltip": "สร้าง checkout ที่แยกอิสระ",
"session.new.workspace.fromBranch": "จาก {{branch}}",
"session.new.workspace.local.description": "ใช้ checkout ปัจจุบัน",
"session.new.workspace.new.description": "ทำงานแบบคู่ขนาน",
"session.new.workspace.existing.description": "เลือกพื้นที่ทำงานที่มีอยู่",
"session.new.workspace.local.tooltip": "เซสชันทำงานในโฟลเดอร์โปรเจกต์ของคุณ",
"session.new.workspace.new.tooltip": "สร้างสำเนาที่แยกออกมาจาก checkout ปัจจุบันของคุณ",
"session.new.workspace.existing.tooltip": "นำพื้นที่ทำงานที่แยกไว้แล้วกลับมาใช้",
"session.new.workspace.trigger.tooltip": "เลือกตำแหน่งที่จะเรียกใช้เซสชัน",
"session.new.workspace.search.placeholder": "ค้นหาพื้นที่ทำงาน",
"settings.tab.workspaces": "พื้นที่ทำงาน",
@ -989,7 +985,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} พื้นที่ทำงาน",
"settings.workspaces.sessions.one": "{{count}} เซสชันใน {{project}}",
"settings.workspaces.sessions.other": "{{count}} เซสชันใน {{project}}",
"settings.workspaces.lastActive": "ใช้งานล่าสุด: {{time}}",
"settings.workspaces.lastActiveSession": "เซสชันที่ใช้งานล่าสุด",
"settings.workspaces.deleteAll": "ลบพื้นที่ทำงานทั้งหมด",
"settings.workspaces.deleteAll.confirm": "ลบพื้นที่ทำงานทั้งหมด {{count}} รายการหรือไม่",
@ -999,7 +994,6 @@ export const dict = {
"พื้นที่ทำงานที่เลือก {{count}} รายการใน {{project}} จะถูกลบอย่างถาวรเมื่อแต่ละรายการสะอาด ไม่ได้ใช้งาน และไม่มีเซสชันที่เชื่อมโยงเท่านั้น",
"settings.workspaces.delete.blocked.active": "ไม่สามารถลบพื้นที่ทำงานที่กำลังใช้งานอยู่ได้",
"settings.workspaces.delete.blocked.linked": "พื้นที่ทำงานนี้มีเซสชันที่เชื่อมโยงอยู่และไม่สามารถลบได้",
"settings.workspaces.section.defaults": "เซสชันใหม่",
"settings.workspaces.default.title": "สภาพแวดล้อมเริ่มต้น",
"settings.workspaces.default.description": "เลือกตำแหน่งเริ่มต้นของเซสชันใหม่",
"settings.workspaces.default.lastUsed": "ใช้ล่าสุดในแต่ละโปรเจกต์",

View file

@ -993,13 +993,9 @@ export const dict = {
"workspace.new": "Yeni çalışma alanı",
"common.viewAll": "Tümünü göster",
"session.new.workspace.local.tooltip": "Mevcut çalışma kopyasını kullan",
"session.new.workspace.new.tooltip": "Yalıtılmış çalışma kopyası oluştur",
"session.new.workspace.fromBranch": "{{branch}} dalından",
"session.new.workspace.local.description": "Mevcut çalışma kopyasını kullan",
"session.new.workspace.new.description": "Paralel çalış",
"session.new.workspace.existing.description": "Mevcut bir çalışma alanını seç",
"session.new.workspace.local.tooltip": "Oturum proje klasörünüzde çalışır",
"session.new.workspace.new.tooltip": "Mevcut çalışma kopyanızdan yalıtılmış bir kopya oluşturur",
"session.new.workspace.existing.tooltip": "Mevcut yalıtılmış bir çalışma alanını yeniden kullan",
"session.new.workspace.trigger.tooltip": "Oturumun nerede çalıştırılacağını seç",
"session.new.workspace.search.placeholder": "Çalışma alanlarında ara",
"settings.tab.workspaces": "Çalışma alanları",
@ -1009,7 +1005,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} çalışma alanı",
"settings.workspaces.sessions.one": "{{project}} projesinde {{count}} oturum",
"settings.workspaces.sessions.other": "{{project}} projesinde {{count}} oturum",
"settings.workspaces.lastActive": "Son etkinlik: {{time}}",
"settings.workspaces.lastActiveSession": "Son etkin oturum",
"settings.workspaces.deleteAll": "Tüm çalışma alanlarını sil",
"settings.workspaces.deleteAll.confirm": "{{count}} çalışma alanının tümü silinsin mi?",
@ -1019,7 +1014,6 @@ export const dict = {
"{{project}} içindeki seçili {{count}} çalışma alanı, yalnızca her biri temiz, etkin değil ve bağlantılı oturumu yoksa kalıcı olarak kaldırılacak.",
"settings.workspaces.delete.blocked.active": "Etkin çalışma alanı silinemez.",
"settings.workspaces.delete.blocked.linked": "Bu çalışma alanında bağlantılı oturumlar var ve silinemez.",
"settings.workspaces.section.defaults": "Yeni oturumlar",
"settings.workspaces.default.title": "Varsayılan ortam",
"settings.workspaces.default.description": "Yeni oturumların nerede başlayacağını seçin",
"settings.workspaces.default.lastUsed": "Proje başına son kullanılan",

View file

@ -1080,13 +1080,9 @@ export const dict = {
"workspace.new": "Нова робоча область",
"common.viewAll": "Показати все",
"session.new.workspace.local.tooltip": "Використати поточну робочу копію",
"session.new.workspace.new.tooltip": "Створити ізольовану копію",
"session.new.workspace.fromBranch": "з {{branch}}",
"session.new.workspace.local.description": "Використати поточну робочу копію",
"session.new.workspace.new.description": "Працювати паралельно",
"session.new.workspace.existing.description": "Вибрати наявну робочу область",
"session.new.workspace.local.tooltip": "Сесія запускається в папці вашого проєкту",
"session.new.workspace.new.tooltip": "Створює ізольовану копію на основі вашої поточної робочої копії",
"session.new.workspace.existing.tooltip": "Повторно використати наявну ізольовану робочу область",
"session.new.workspace.trigger.tooltip": "Вибрати, де запустити сесію",
"session.new.workspace.search.placeholder": "Пошук робочих областей",
"settings.tab.workspaces": "Робочі простори",
@ -1096,7 +1092,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} робочих просторів",
"settings.workspaces.sessions.one": "{{count}} сесія в {{project}}",
"settings.workspaces.sessions.other": "{{count}} сесій у {{project}}",
"settings.workspaces.lastActive": "Остання активність: {{time}}",
"settings.workspaces.lastActiveSession": "Останній активний сеанс",
"settings.workspaces.deleteAll": "Видалити всі робочі простори",
"settings.workspaces.deleteAll.confirm": "Видалити всі робочі простори ({{count}})?",
@ -1106,7 +1101,6 @@ export const dict = {
"Вибрані робочі простори ({{count}}) у {{project}} буде видалено назавжди, лише якщо кожен із них чистий, неактивний і не має пов’язаних сеансів.",
"settings.workspaces.delete.blocked.active": "Активний робочий простір не можна видалити.",
"settings.workspaces.delete.blocked.linked": "Цей робочий простір має пов’язані сеанси, тому його не можна видалити.",
"settings.workspaces.section.defaults": "Нові сеанси",
"settings.workspaces.default.title": "Середовище за замовчуванням",
"settings.workspaces.default.description": "Виберіть, де починатимуться нові сеанси",
"settings.workspaces.default.lastUsed": "Останнє використане для кожного проєкту",

View file

@ -966,13 +966,9 @@ export const dict = {
"workspace.new": "新建工作区",
"common.viewAll": "查看全部",
"session.new.workspace.local.tooltip": "使用当前工作副本",
"session.new.workspace.new.tooltip": "创建隔离副本",
"session.new.workspace.fromBranch": "从 {{branch}}",
"session.new.workspace.local.description": "使用当前工作副本",
"session.new.workspace.new.description": "并行工作",
"session.new.workspace.existing.description": "从现有工作区中选择",
"session.new.workspace.local.tooltip": "会话在你的项目文件夹中运行",
"session.new.workspace.new.tooltip": "基于你当前的工作副本创建隔离副本",
"session.new.workspace.existing.tooltip": "复用现有的隔离工作区",
"session.new.workspace.trigger.tooltip": "选择会话的运行位置",
"session.new.workspace.search.placeholder": "搜索工作区",
"settings.tab.workspaces": "工作区",
@ -982,7 +978,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} 个工作区",
"settings.workspaces.sessions.one": "{{project}} 中有 {{count}} 个会话",
"settings.workspaces.sessions.other": "{{project}} 中有 {{count}} 个会话",
"settings.workspaces.lastActive": "上次活跃:{{time}}",
"settings.workspaces.lastActiveSession": "最近活跃的会话",
"settings.workspaces.deleteAll": "删除所有工作区",
"settings.workspaces.deleteAll.confirm": "删除全部 {{count}} 个工作区?",
@ -992,7 +987,6 @@ export const dict = {
"仅当 {{project}} 中选定的 {{count}} 个工作区均干净、未处于活动状态且没有关联会话时,才会永久删除它们。",
"settings.workspaces.delete.blocked.active": "无法删除当前活动的工作区。",
"settings.workspaces.delete.blocked.linked": "此工作区有关联会话,无法删除。",
"settings.workspaces.section.defaults": "新会话",
"settings.workspaces.default.title": "默认环境",
"settings.workspaces.default.description": "选择新会话的启动位置",
"settings.workspaces.default.lastUsed": "每个项目上次使用的环境",

View file

@ -962,13 +962,9 @@ export const dict = {
"workspace.new": "新增工作區",
"common.viewAll": "查看全部",
"session.new.workspace.local.tooltip": "使用目前的工作複本",
"session.new.workspace.new.tooltip": "建立隔離複本",
"session.new.workspace.fromBranch": "從 {{branch}}",
"session.new.workspace.local.description": "使用目前的工作複本",
"session.new.workspace.new.description": "平行作業",
"session.new.workspace.existing.description": "從現有工作區中選擇",
"session.new.workspace.local.tooltip": "工作階段會在你的專案資料夾中執行",
"session.new.workspace.new.tooltip": "根據你目前的工作複本建立隔離複本",
"session.new.workspace.existing.tooltip": "重新使用現有的隔離工作區",
"session.new.workspace.trigger.tooltip": "選擇工作階段的執行位置",
"session.new.workspace.search.placeholder": "搜尋工作區",
"settings.tab.workspaces": "工作區",
@ -978,7 +974,6 @@ export const dict = {
"settings.workspaces.count.other": "{{count}} 個工作區",
"settings.workspaces.sessions.one": "{{project}} 中有 {{count}} 個工作階段",
"settings.workspaces.sessions.other": "{{project}} 中有 {{count}} 個工作階段",
"settings.workspaces.lastActive": "上次活動:{{time}}",
"settings.workspaces.lastActiveSession": "最近活動的工作階段",
"settings.workspaces.deleteAll": "刪除所有工作區",
"settings.workspaces.deleteAll.confirm": "刪除全部 {{count}} 個工作區?",
@ -988,7 +983,6 @@ export const dict = {
"只有在 {{project}} 中選取的 {{count}} 個工作區都乾淨、未處於活動狀態且沒有關聯工作階段時,才會永久刪除它們。",
"settings.workspaces.delete.blocked.active": "無法刪除目前作用中的工作區。",
"settings.workspaces.delete.blocked.linked": "此工作區有關聯工作階段,無法刪除。",
"settings.workspaces.section.defaults": "新工作階段",
"settings.workspaces.default.title": "預設環境",
"settings.workspaces.default.description": "選擇新工作階段的啟動位置",
"settings.workspaces.default.lastUsed": "每個專案上次使用的環境",

View file

@ -1119,7 +1119,7 @@ export default function LegacyLayout(props: ParentProps) {
: import("@/components/dialog-settings")
void module.then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSettings />)
dialog.show(() => <x.DialogSettings sessionID={params.id} />)
})
}

View file

@ -31,6 +31,19 @@ export function NewSessionView(props: {
project: PromptProjectController
workspace: NewSessionWorkspaceController
}) {
const language = useLanguage()
const [onboarding, setOnboarding, , onboardingReady] = persisted(
Persist.global("workspace-onboarding"),
createStore({ cardDismissed: false, used: false }),
)
const showCard = createMemo(
() => onboardingReady() && props.workspace.bar.visible() && !onboarding.cardDismissed && !onboarding.used,
)
const select = (value: string) => {
props.workspace.selection.set(value)
setOnboarding("used", true)
}
return (
<div class="@container relative flex flex-col min-h-0 h-full flex-1">
<div
@ -59,7 +72,8 @@ export function NewSessionView(props: {
projectRoot={props.workspace.project.root()}
workspaces={props.workspace.project.workspaces()}
branch={props.workspace.bar.branch()}
onChange={props.workspace.selection.set}
onboarding={onboarding.cardDismissed && !onboarding.used}
onChange={select}
onDone={props.input.restoreFocus}
onViewAll={props.workspace.project.openAll}
/>
@ -69,6 +83,27 @@ export function NewSessionView(props: {
</div>
</div>
</div>
<Show when={showCard()}>
<div class="absolute inset-x-0 bottom-[calc(25%-81px)] flex justify-center px-6 pointer-events-none">
<div class="pointer-events-auto flex w-[280px] flex-col gap-2 rounded-lg bg-v2-background-bg-base p-3 shadow-[var(--v2-elevation-floating)]">
<div class="flex items-start gap-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-muted">
<IconV2 name="workspace-isolated" class="shrink-0" />
<span class="min-w-0 flex-1">{language.t("workspace.onboarding.title")}</span>
<button
type="button"
class="flex size-5 shrink-0 items-center justify-center rounded-[4px] text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-icon-icon-base focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
aria-label={language.t("common.dismiss")}
onClick={() => setOnboarding("cardDismissed", true)}
>
<IconV2 name="xmark-small" />
</button>
</div>
<p class="text-[13px] font-[440] leading-4 tracking-[-0.04px] text-v2-text-text-muted">
{language.t("workspace.onboarding.description")}
</p>
</div>
</div>
</Show>
<ProviderTip />
</div>
</div>

View file

@ -5,7 +5,7 @@ import { useServerSync } from "@/context/server-sync"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { pathKey } from "@/utils/path-key"
import { isWorkspaceSelection, workspaceDefaultSelection } from "@/utils/workspace"
import { isWorkspaceSelection, workspaceDefaultSelection, workspaceDirectories } from "@/utils/workspace"
export function resolveNewSessionWorktree(input: {
enabled: boolean
@ -96,7 +96,10 @@ export function createNewSessionWorkspaceController(input: {
},
project: {
root: projectRoot,
workspaces: () => sync().project?.sandboxes ?? [],
workspaces: () => {
const project = sync().project
return project ? workspaceDirectories(project) : []
},
git: () => sync().project?.vcs === "git",
openAll: input.onViewAll,
},

View file

@ -271,8 +271,8 @@ function WorkspaceMoveAction(props: {
<div
classList={{
"group/workspace-move relative shrink-0": true,
"ml-auto h-6 w-[155px]": inline(),
"h-[46px] w-full bg-v2-background-bg-layer-02 rounded-b-[6px]": !inline(),
"ml-auto h-5 w-[167px]": inline(),
"h-[46px] w-full rounded-b-[6px] bg-v2-background-bg-layer-02 hover:bg-v2-background-bg-layer-03": !inline(),
invisible: props.dismissed,
}}
>
@ -286,16 +286,20 @@ function WorkspaceMoveAction(props: {
gutter={inline() ? 4 : -2}
class={
inline()
? "flex h-6 w-full items-center gap-1.5 rounded-[4px] px-1.5 pr-7 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed"
: "flex h-[46px] w-full items-center gap-1.5 rounded-b-[6px] px-3 pr-9 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed"
? "flex h-5 w-full items-center gap-1.5 rounded-[4px] pr-6 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed"
: "flex h-[46px] w-full items-center gap-1.5 rounded-b-[6px] px-3 pr-9 pt-[10px] text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted focus-visible:outline-none data-[expanded]:bg-v2-background-bg-layer-03"
}
>
<IconV2 name="workspace" class="shrink-0 text-v2-icon-icon-muted" />
<IconV2 name={inline() ? "workspace-new" : "workspace"} class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 truncate">{language.t("workspace.move.title")}</span>
</SessionWorkspaceMenu>
<button
type="button"
class="hover-reveal absolute right-1 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-[4px] text-v2-icon-icon-muted group-hover/workspace-move:opacity-100 group-focus-within/workspace-move:opacity-100 hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-icon-icon-base focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:text-v2-icon-icon-base focus-visible:outline-none"
class={`absolute flex size-5 -translate-y-1/2 items-center justify-center rounded-[4px] text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-icon-icon-base focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:text-v2-icon-icon-base focus-visible:outline-none ${
inline()
? "right-0 top-1/2"
: "hover-reveal right-3 top-[calc(50%+5px)] group-hover/workspace-move:opacity-100 group-focus-within/workspace-move:opacity-100"
}`}
aria-label={language.t("common.dismiss")}
onClick={(event) => {
event.stopPropagation()
@ -361,7 +365,7 @@ function SessionSummaryPanel(props: {
class={`${row} hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed`}
>
<IconV2 name="monitor" class="shrink-0 text-v2-icon-icon-muted" />
<span class="min-w-0 flex-1 truncate">{location()}</span>
<span class="min-w-0 flex-1 truncate text-left">{location()}</span>
<IconV2 name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</SessionWorkspaceMenu>
</Show>
@ -514,6 +518,21 @@ export function MessageTimeline(props: {
if (!id) return
return WorkspaceOperation.get(serverSDK().scope, id)
})
const lifecycleTitle = createMemo(() => {
const operation = workspaceOperation()
if (operation?.status === "pending") {
return {
kind: "pending" as const,
workspace: true,
text: language.t(operation.type === "create" ? "workspace.lifecycle.creating" : "workspace.lifecycle.moving"),
}
}
if (operation?.type === "create" && titleValue()?.startsWith("New session"))
return { kind: "created" as const, workspace: true, text: language.t("workspace.lifecycle.created") }
if (titleValue()?.startsWith("New session"))
return { kind: "starting" as const, workspace: workspaceSession(), text: language.t("workspace.lifecycle.starting") }
return
})
const workspaceOperationPending = (sessionID: string) =>
WorkspaceOperation.get(serverSDK().scope, sessionID)?.status === "pending"
const shareUrl = createMemo(() => info()?.share?.url)
@ -1782,57 +1801,73 @@ export function MessageTimeline(props: {
/
</span>
</Show>
<Show when={childTitle() || title.editing}>
<Show
when={title.editing}
fallback={
<h1
data-slot="session-title-child"
classList={{
"truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
"w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
settings.general.newLayoutDesigns(),
"grow-1 min-w-0": !settings.general.newLayoutDesigns(),
<Show
when={!lifecycleTitle()}
fallback={
<span
class="px-2 text-[13px] font-[530] leading-4 tracking-[-0.04px]"
classList={{ "text-v2-text-text-base": lifecycleTitle()?.kind === "created" }}
aria-live="polite"
>
<Show when={lifecycleTitle()?.kind !== "created"} fallback={lifecycleTitle()?.text}>
<TextShimmer text={lifecycleTitle()!.text} />
</Show>
</span>
}
>
<Show when={childTitle() || title.editing}>
<Show
when={title.editing}
fallback={
<h1
data-slot="session-title-child"
classList={{
"truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
"w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
settings.general.newLayoutDesigns(),
"grow-1 min-w-0": !settings.general.newLayoutDesigns(),
}}
onClick={openTitleEditor}
>
{childTitle()}
</h1>
}
>
<InlineInput
ref={(el) => {
titleRef = el
}}
onClick={openTitleEditor}
>
{childTitle()}
</h1>
}
>
<InlineInput
ref={(el) => {
titleRef = el
}}
data-slot="session-title-child"
value={title.draft}
disabled={titleMutation.isPending}
classList={{
"block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
"w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
"field-sizing-content self-start rounded-[6px] px-2 py-1 ":
settings.general.newLayoutDesigns(),
}}
style={{
"--inline-input-shadow": settings.general.newLayoutDesigns()
? "none"
: "var(--shadow-xs-border-select)",
}}
onInput={(event) => setTitle("draft", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
if (event.key === "Enter") {
event.preventDefault()
void saveTitleEditor()
return
}
if (event.key === "Escape") {
event.preventDefault()
closeTitleEditor()
}
}}
onBlur={closeTitleEditor}
/>
data-slot="session-title-child"
value={title.draft}
disabled={titleMutation.isPending}
classList={{
"block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
"w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]":
!settings.general.newLayoutDesigns(),
"field-sizing-content self-start rounded-[6px] px-2 py-1 ":
settings.general.newLayoutDesigns(),
}}
style={{
"--inline-input-shadow": settings.general.newLayoutDesigns()
? "none"
: "var(--shadow-xs-border-select)",
}}
onInput={(event) => setTitle("draft", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
if (event.key === "Enter") {
event.preventDefault()
void saveTitleEditor()
return
}
if (event.key === "Escape") {
event.preventDefault()
closeTitleEditor()
}
}}
onBlur={closeTitleEditor}
/>
</Show>
</Show>
</Show>
</div>

View file

@ -11,6 +11,7 @@ import {
runWorkspaceDeleteTransaction,
sessionsForWorkspace,
workspaceDefaultSelection,
workspaceDirectories,
workspaceInventory,
} from "./workspace"
@ -73,7 +74,7 @@ describe("isProjectDirectory", () => {
test("groups and filters workspace inventory by project", () => {
const inventory = workspaceInventory([
{ id: "a", worktree: "/a", sandboxes: ["/a/one", "/a/two"] },
{ id: "a", worktree: "/a", sandboxes: ["/a", "/a/one", "/a/two"] },
{ id: "b", worktree: "/b", sandboxes: ["/b/one"] },
])
@ -86,6 +87,12 @@ test("groups and filters workspace inventory by project", () => {
expect(filterWorkspaceInventory(inventory, "all")).toEqual(inventory)
})
test("excludes the project checkout from workspace directories", () => {
expect(workspaceDirectories({ worktree: "C:\\repo", sandboxes: ["c:\\repo\\", "C:\\workspaces\\one"] })).toEqual([
"C:\\workspaces\\one",
])
})
test("deletes all workspaces sequentially", async () => {
const calls: string[] = []
let release = () => {}
@ -131,6 +138,21 @@ test("blocks unsafe workspace deletion", () => {
status: "clean",
}),
).toBe("linked")
expect(
inspectWorkspaceDeletion({
workspace: "/workspace",
activeDirectory: "/workspace/app",
sessions: [],
status: "dirty",
}),
).toBe("active")
expect(
inspectWorkspaceDeletion({
workspace: "/workspace",
sessions: [session("/workspace/packages/app")],
status: "dirty",
}),
).toBe("linked")
expect(inspectWorkspaceDeletion({ workspace: "/workspace", sessions: [], status: "dirty" })).toBe("dirty")
expect(inspectWorkspaceDeletion({ workspace: "/workspace", sessions: [], status: "unknown" })).toBe("unknown")
expect(inspectWorkspaceDeletion({ workspace: "/workspace", sessions: [], status: "clean" })).toBe("safe")

View file

@ -4,8 +4,14 @@ import type { Session } from "@opencode-ai/sdk/v2/client"
type WorkspaceProject = { worktree: string; sandboxes?: readonly string[] }
export function workspaceDirectories(project: WorkspaceProject) {
return (project.sandboxes ?? []).filter(
(directory) => !containsDirectory(project.worktree, directory) || !containsDirectory(directory, project.worktree),
)
}
export function workspaceInventory<T extends WorkspaceProject & { id: string }>(projects: readonly T[]) {
return projects.flatMap((project) => (project.sandboxes ?? []).map((directory) => ({ directory, project })))
return projects.flatMap((project) => workspaceDirectories(project).map((directory) => ({ directory, project })))
}
export function filterWorkspaceInventory<T extends { project: { id: string } }>(
@ -66,16 +72,16 @@ export function inspectWorkspaceDeletion(input: {
sessions: readonly Session[]
status: "clean" | "dirty" | "unknown"
}): WorkspaceDeleteInspection {
if (input.status === "unknown") return "unknown"
if (input.status === "dirty") return "dirty"
if (input.activeDirectory && containsDirectory(input.workspace, input.activeDirectory)) return "active"
if (input.sessions.some((session) => containsDirectory(input.workspace, session.directory))) return "linked"
if (input.status === "unknown") return "unknown"
if (input.status === "dirty") return "dirty"
return "safe"
}
export function isWorkspaceDirectory(project: WorkspaceProject | undefined, directory: string) {
if (!project || containsDirectory(project.worktree, directory)) return false
return project.sandboxes?.some((workspace) => containsDirectory(workspace, directory)) ?? false
return workspaceDirectories(project).some((workspace) => containsDirectory(workspace, directory))
}
export function isProjectDirectory(project: WorkspaceProject | undefined, directory: string) {

View file

@ -93,6 +93,9 @@
[data-slot="dialog-description"] {
flex: none;
flex-grow: 0;
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
user-select: none;
font-weight: 440;
font-size: 13px;