From 6282204f07f7d434b1479ac9a1e272ee75ce11dc Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 17 May 2026 11:13:23 -0500 Subject: [PATCH 1/3] feat: add workspace_rename IPC handler, commands.ts export, and mock CLI support Wire up workspace renaming through the desktop app's IPC layer: - Add workspace_rename handler in ipc.ts with telemetry tracking - Export workspaceRename function from commands.ts - Add rename command routing in mock-devsy.cjs for e2e tests --- desktop/e2e/fixtures/mock-devsy.cjs | 14 ++++++++++++++ desktop/src/main/ipc.ts | 11 +++++++++++ desktop/src/renderer/src/lib/ipc/commands.ts | 7 +++++++ 3 files changed, 32 insertions(+) diff --git a/desktop/e2e/fixtures/mock-devsy.cjs b/desktop/e2e/fixtures/mock-devsy.cjs index d2b3948d4..541148a5d 100755 --- a/desktop/e2e/fixtures/mock-devsy.cjs +++ b/desktop/e2e/fixtures/mock-devsy.cjs @@ -392,6 +392,20 @@ switch (cmd) { break } + case "rename": { + const oldId = sub + const newId = extra + if (oldId && newId) { + const idx = state.workspaces.findIndex((w) => w.id === oldId) + if (idx !== -1) { + state.workspaces[idx].id = newId + saveState(state) + } + } + out("") + break + } + case "upgrade": out("Already up to date.") process.exit(0) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index fbc4ebf00..e79b01db5 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -58,6 +58,17 @@ export function registerIpcHandlers(deps: IpcDependencies): void { }, ) + ipcMain.handle( + "workspace_rename", + async ( + _event, + args: { workspaceId: string; newWorkspaceId: string }, + ) => { + trackEvent("workspace_rename", { workspaceId: args.workspaceId }) + await cli.runRaw(["rename", args.workspaceId, args.newWorkspaceId]) + }, + ) + // ── Providers ── ipcMain.handle("provider_list", async () => { const raw = await cli.run>([ diff --git a/desktop/src/renderer/src/lib/ipc/commands.ts b/desktop/src/renderer/src/lib/ipc/commands.ts index abdeb2635..e35ef2fe4 100644 --- a/desktop/src/renderer/src/lib/ipc/commands.ts +++ b/desktop/src/renderer/src/lib/ipc/commands.ts @@ -58,6 +58,13 @@ export async function workspaceStatus(workspaceId: string): Promise { return invoke("workspace_status", { workspaceId }) } +export async function workspaceRename( + workspaceId: string, + newWorkspaceId: string, +): Promise { + return invoke("workspace_rename", { workspaceId, newWorkspaceId }) +} + // Provider commands export async function providerList(): Promise { return invoke("provider_list") From a479d0b74b7c6f442d9835fed35e5c83012e78ac Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 17 May 2026 11:20:40 -0500 Subject: [PATCH 2/3] feat: add inline workspace rename UI and e2e test Add inline editable workspace name in WorkspaceDetailPage header with pencil icon trigger, Input/Save/Cancel form, and navigation to the new workspace URL after rename. Add e2e test covering the full rename flow. --- desktop/e2e/integration.e2e.ts | 31 +++++++++-- .../src/pages/WorkspaceDetailPage.svelte | 54 ++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/desktop/e2e/integration.e2e.ts b/desktop/e2e/integration.e2e.ts index ed33e7848..86fd5e442 100644 --- a/desktop/e2e/integration.e2e.ts +++ b/desktop/e2e/integration.e2e.ts @@ -300,6 +300,28 @@ test.describe await expect(headerArea).toContainText("Stopped", { timeout: 10000 }) }) + test("can rename a workspace", async () => { + // We're on the node-js detail page after stopping it + + // Click the rename (pencil) button + await page.locator('[data-slot="workspace-rename-btn"]').click() + + // Fill the rename input with new name + const renameInput = page.locator('[data-slot="workspace-rename-input"]') + await renameInput.waitFor({ timeout: 5000 }) + await renameInput.fill("node-js-renamed") + + // Click Save + await page.locator('[data-slot="workspace-rename-save"]').click() + + // Wait for rename to complete and navigation to new URL + await page.waitForTimeout(4000) + + // Verify the new name appears in the header + const headerArea = page.locator("h1", { hasText: "node-js-renamed" }) + await expect(headerArea).toBeVisible({ timeout: 10000 }) + }) + test("should delete workspace from detail page", async () => { // Open the More actions dropdown, then click Delete await page.getByRole("button", { name: "More actions" }).click() @@ -313,10 +335,11 @@ test.describe // Navigates to /workspaces on success — wait for table await page.locator("table").waitFor({ timeout: 15000 }) - // Verify node-js is gone - await expect(page.locator("table")).not.toContainText("node-js", { - timeout: 10000, - }) + // Verify renamed workspace is gone + await expect(page.locator("table")).not.toContainText( + "node-js-renamed", + { timeout: 10000 }, + ) }) }) diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index 2c6c8b3e5..5e22b6d4e 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -7,6 +7,7 @@ import { ClipboardCopy, Ellipsis, Monitor, + Pencil, Play, RefreshCw, RotateCcw, @@ -17,6 +18,7 @@ import { import * as Tooltip from "$lib/components/ui/tooltip/index.js" import { Spinner } from "$lib/components/ui/spinner/index.js" import { Button } from "$lib/components/ui/button/index.js" +import { Input } from "$lib/components/ui/input/index.js" import * as ButtonGroup from "$lib/components/ui/button-group/index.js" import { badgeVariants } from "$lib/components/ui/badge/index.js" import * as Command from "$lib/components/ui/command/index.js" @@ -38,6 +40,7 @@ import { workspaceRebuild, workspaceReset, workspaceDelete, + workspaceRename, workspaceLogsList, workspaceLogRead, workspaceLogDelete, @@ -121,6 +124,9 @@ let connecting = $state(false) let ideComboOpen = $state(false) let ideSearch = $state("") let selectedIde = $state(null) +let renaming = $state(false) +let renameValue = $state("") +let renameSaving = $state(false) let currentIde = $derived(selectedIde ?? workspace?.ide?.name ?? "none") let filteredIdes = $derived( IDE_OPTIONS.filter((i) => @@ -357,6 +363,30 @@ async function handleDelete() { toasts.error(`Failed to delete: ${extractErrorMessage(err)}`) } } + +function startRename() { + renameValue = id + renaming = true +} + +async function handleRename() { + const trimmed = renameValue.trim() + if (!trimmed || trimmed === id) { + renaming = false + return + } + renameSaving = true + try { + await workspaceRename(id, trimmed) + toasts.success(`Renamed workspace to ${trimmed}`) + renaming = false + goto(`/workspaces/${trimmed}`) + } catch (err) { + toasts.error(`Failed to rename: ${extractErrorMessage(err)}`) + } finally { + renameSaving = false + } +}
@@ -364,7 +394,29 @@ async function handleDelete() { -

{id}

+ {#if renaming} +
{ e.preventDefault(); handleRename() }}> + (renameValue = e.currentTarget.value)} + class="h-8 w-56 text-lg font-bold" + disabled={renameSaving} + /> + + +
+ {:else} +

{id}

+ + {/if} {#if workspace?.provider?.name} {workspace.provider.name} {/if} From e238eeabda19f6fe050f584d709d9bfdaa31cf9a Mon Sep 17 00:00:00 2001 From: Samuel K Date: Sun, 17 May 2026 11:25:15 -0500 Subject: [PATCH 3/3] fix: disable rename button while operation is running --- desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index 5e22b6d4e..7523ecc66 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -412,7 +412,7 @@ async function handleRename() { {:else}

{id}

-