diff --git a/packages/app/e2e/regression/workspaces.spec.ts b/packages/app/e2e/regression/workspaces.spec.ts index 57f04f2f495..92bb667552a 100644 --- a/packages/app/e2e/regression/workspaces.spec.ts +++ b/packages/app/e2e/regression/workspaces.spec.ts @@ -104,7 +104,9 @@ test("selects local, new, and existing workspaces from the ready-ish start menu" await trigger.click() const newWorkspace = page.getByRole("menuitem", { name: /New workspace/ }) await expect(newWorkspace).toBeVisible() - await expect(page.getByRole("menuitem", { name: /Workspace/ })).toBeVisible() + const workspaceTrigger = page.getByRole("menuitem", { name: /Workspace/ }) + await expect(workspaceTrigger).toBeVisible() + await workspaceTrigger.hover() await expect(page.getByRole("menuitem", { name: "View all" })).toBeVisible() await newWorkspace.click() @@ -133,7 +135,7 @@ test("searches long workspace lists within the available viewport", async ({ pag await page.goto(`/new-session?draftId=${draftID}`) await expectAppVisible(page.locator('[data-component="prompt-input"]')) - await page.getByRole("button", { name: /^local$/i }).click() + await page.getByRole("button", { name: /local|new workspace/i }).click() await page.getByRole("menuitem", { name: /Workspace/ }).focus() await page.keyboard.press("ArrowRight") @@ -203,7 +205,9 @@ test("lists and manually deletes workspaces from settings", async ({ page }) => await page.goto(`/new-session?draftId=${draftID}`) await expectAppVisible(page.locator('[data-component="prompt-input"]')) - await page.getByRole("button", { name: /^local$/i }).click() + await page.getByRole("button", { name: /local|new workspace/i }).click() + const workspacesTrigger = page.getByRole("menuitem", { name: /Workspace/ }) + if (await workspacesTrigger.isVisible()) await workspacesTrigger.hover() await page.getByRole("menuitem", { name: "View all" }).click() const settings = page.locator(".settings-v2-dialog") @@ -215,7 +219,8 @@ test("lists and manually deletes workspaces from settings", async ({ page }) => const firstSessionInventory = sessionListRequests await page.keyboard.press("Escape") await expect(settings).toHaveCount(0) - await page.getByRole("button", { name: /^local$/i }).click() + await page.getByRole("button", { name: /local|new workspace/i }).click() + if (await workspacesTrigger.isVisible()) await workspacesTrigger.hover() await page.getByRole("menuitem", { name: "View all" }).click() await expect(settings.getByText("Workspace settings session", { exact: true })).toBeVisible() expect(sessionListRequests).toBeGreaterThan(firstSessionInventory) @@ -273,6 +278,8 @@ test("blocks deletion of the currently active workspace", async ({ page }) => { await page.goto(`/new-session?draftId=${draftID}`) await page.getByRole("button", { name: /feature/ }).click() + const workspacesTrigger = page.getByRole("menuitem", { name: /Workspace/ }) + if (await workspacesTrigger.isVisible()) await workspacesTrigger.hover() await page.getByRole("menuitem", { name: "View all" }).click() const settings = page.locator(".settings-v2-dialog") await settings.getByRole("button", { name: 'Delete workspace "feature"?' }).click() @@ -313,7 +320,9 @@ test("wraps the workspace toolbar for long project filters on mobile", async ({ await page.goto(`/new-session?draftId=${draftID}`) const dismissTabs = page.getByRole("button", { name: "Dismiss Tabs information" }) if (await dismissTabs.isVisible()) await dismissTabs.click() - await page.getByRole("button", { name: /^local$/i }).click() + await page.getByRole("button", { name: /local|new workspace/i }).click() + const workspacesTrigger = page.getByRole("menuitem", { name: /Workspace/ }) + if (await workspacesTrigger.isVisible()) await workspacesTrigger.hover() await page.getByRole("menuitem", { name: "View all" }).click() const settings = page.locator(".settings-v2-dialog") await expect(settings.getByRole("button", { name: "All projects" })).toBeVisible() @@ -389,7 +398,9 @@ test("bulk deletion snapshots inventory and skips dirty or unknown workspaces", await page.goto(`/new-session?draftId=${draftID}`) await transport.waitForConnection() - await page.getByRole("button", { name: /^local$/i }).click() + await page.getByRole("button", { name: /local|new workspace/i }).click() + const workspacesTrigger = page.getByRole("menuitem", { name: /Workspace/ }) + if (await workspacesTrigger.isVisible()) await workspacesTrigger.hover() await page.getByRole("menuitem", { name: "View all" }).click() const settings = page.locator(".settings-v2-dialog") await settings.getByRole("button", { name: "More options" }).click() diff --git a/packages/app/src/components/prompt-workspace-selector.tsx b/packages/app/src/components/prompt-workspace-selector.tsx index 9b8a87cd4b3..55748e04238 100644 --- a/packages/app/src/components/prompt-workspace-selector.tsx +++ b/packages/app/src/components/prompt-workspace-selector.tsx @@ -129,7 +129,17 @@ export function PromptWorkspaceSelector(props: { - 0}> + 0} + fallback={ + <> + + (pending = { type: "viewAll" })}> + {language.t("common.viewAll")} + + > + } + > )} + + (pending = { type: "viewAll" })}> + {language.t("common.viewAll")} + - - (pending = { type: "viewAll" })}> - {language.t("common.viewAll")} - @@ -225,12 +235,12 @@ export function PromptWorkspaceSelector(props: { - + > ) } -export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?: boolean; connected?: boolean }) { +export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?: boolean }) { const language = useLanguage() const label = () => { if (props.noGit) return language.t("session.new.git.none") @@ -239,32 +249,30 @@ export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from? return props.branch } + const icon = () => { + if (props.noGit) return "monitor" + if (props.from) return "branch-out" + return "branch" + } + return ( {(value) => ( - <> - - / - - - - - {value()} - - - > + + + + {value()} + + )} ) diff --git a/packages/app/src/components/session-workspace-menu.tsx b/packages/app/src/components/session-workspace-menu.tsx index 6c35e06941e..26119fa2156 100644 --- a/packages/app/src/components/session-workspace-menu.tsx +++ b/packages/app/src/components/session-workspace-menu.tsx @@ -7,6 +7,7 @@ import { For, Show, type ComponentProps, type JSX } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" +import { useSettingsDialog } from "@/components/settings-dialog" import { pathKey } from "@/utils/path-key" import { Worktree } from "@/utils/worktree" import { WorkspaceOperation } from "@/utils/workspace-operation" @@ -35,6 +36,7 @@ export function SessionWorkspaceMenu(props: { const language = useLanguage() const serverSDK = useServerSDK() const serverSync = useServerSync() + const openWorkspaces = useSettingsDialog("workspaces") const [store, setStore] = createStore({ selected: undefined as string | undefined }) const operationPending = () => WorkspaceOperation.get(serverSDK().scope, props.sessionID)?.status === "pending" const blocked = () => @@ -126,28 +128,31 @@ export function SessionWorkspaceMenu(props: { {language.t("workspace.new")} + 0}> + + + + {language.t("session.new.workspace.existing").replace(/…$/, "")} + + + + + {(workspace) => ( + void move(workspace)}> + + {getFilename(workspace)} + + )} + + + + + - 0}> - - - - - {language.t("session.new.workspace.existing").replace(/…$/, "")} - - - - - {(workspace) => ( - void move(workspace)}> - - {getFilename(workspace)} - - )} - - - - - + + openWorkspaces()}> + {language.t("common.viewAll")} + diff --git a/packages/app/src/components/settings-v2/settings-v2.css b/packages/app/src/components/settings-v2/settings-v2.css index 2ab8e0a6625..db4e98c078e 100644 --- a/packages/app/src/components/settings-v2/settings-v2.css +++ b/packages/app/src/components/settings-v2/settings-v2.css @@ -784,8 +784,11 @@ .settings-v2-workspaces-sessions { display: flex; flex-direction: column; - gap: 6px; - padding-top: 4px; + border: 0.5px solid var(--v2-border-border-base); + border-radius: 4px; + background-color: var(--v2-background-bg-base); + overflow: hidden; + margin-top: 4px; } .settings-v2-workspaces-session { @@ -794,26 +797,29 @@ align-items: center; justify-content: space-between; gap: 12px; - overflow: hidden; + padding: 8px 12px; font-size: 13px; font-weight: 440; line-height: 16px; color: var(--v2-text-text-base); - text-overflow: ellipsis; - white-space: nowrap; +} + +.settings-v2-workspaces-session:not(:last-child) { + border-bottom: 0.5px solid var(--v2-border-border-base); } .settings-v2-workspaces-session > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; } -.settings-v2-workspaces-session-active { +.settings-v2-workspaces-session-time { flex-shrink: 0; - color: var(--v2-text-text-muted); font-size: 11px; - line-height: 14px; + line-height: 1; + color: var(--v2-text-text-muted); } .settings-v2-workspaces-empty { diff --git a/packages/app/src/components/settings-v2/workspaces.tsx b/packages/app/src/components/settings-v2/workspaces.tsx index ecc904673db..d1771ed87b5 100644 --- a/packages/app/src/components/settings-v2/workspaces.tsx +++ b/packages/app/src/components/settings-v2/workspaces.tsx @@ -107,6 +107,10 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr if (!updated) return undefined return getRelativeTime(new Date(updated).toISOString(), language.t) } + const sessionTime = (session: Session) => { + if (!session.time.updated) return undefined + return getRelativeTime(new Date(session.time.updated).toISOString(), language.t) + } const inspect = async (workspace: Workspace, context = captureDeleteContext()) => { const [status, sessions] = await Promise.all([ @@ -331,22 +335,29 @@ export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (pr - {(value) => {value()}} + {(value) => ( + + + {value()} + + + )} {sessionCount(workspace)} 0}> - {(session, index) => ( - + {(session) => ( + {session.title} - - - {language.t("settings.workspaces.lastActiveSession")} - + + {(time) => {time()}} - + )} diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 3ee7aaa7341..24fbcce2aac 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -269,7 +269,7 @@ function WorkspaceMoveAction(props: { classList={{ "group/workspace-move relative shrink-0": true, "ml-auto h-5 w-[167px]": inline(), - "-mt-2.5 h-[46px] w-full rounded-b-[6px] bg-v2-background-bg-layer-02 hover:bg-v2-background-bg-layer-03": + "-mt-2.5 h-[46px] w-full rounded-b-[6px] bg-v2-background-bg-layer-02 hover:bg-v2-background-bg-layer-03 transition-colors": !inline(), invisible: props.dismissed, }} @@ -286,10 +286,10 @@ function WorkspaceMoveAction(props: { class={ inline() ? "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" + : "flex h-[46px] w-full items-center gap-2 rounded-b-[6px] px-3 pr-9 pt-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted focus-visible:outline-none" } > - + {language.t("workspace.move.title")} - {displayName(props.project)} + {displayName(props.project)} - - - {location()} - - } + - - - {location()} - - - + + {location()} + + - - - }> - + + + + } + > + - - - - - {directory()} - - + {status()} - + + · + + + {directory()} + + + - diff --git a/packages/ui/src/v2/components/icon.tsx b/packages/ui/src/v2/components/icon.tsx index b9f35037d57..2b933bcabd1 100644 --- a/packages/ui/src/v2/components/icon.tsx +++ b/packages/ui/src/v2/components/icon.tsx @@ -18,6 +18,10 @@ const icons = { viewBox: "0 0 16 16", body: ``, }, + "branch-out": { + viewBox: "0 0 16 16", + body: ``, + }, "grid-plus": { viewBox: "0 0 16 16", body: ``,