Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions desktop/e2e/fixtures/mock-devsy.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 27 additions & 4 deletions desktop/e2e/integration.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 },
)
})
})

Expand Down
11 changes: 11 additions & 0 deletions desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, ProviderEntry>>([
Expand Down
7 changes: 7 additions & 0 deletions desktop/src/renderer/src/lib/ipc/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export async function workspaceStatus(workspaceId: string): Promise<string> {
return invoke<string>("workspace_status", { workspaceId })
}

export async function workspaceRename(
workspaceId: string,
newWorkspaceId: string,
): Promise<void> {
return invoke("workspace_rename", { workspaceId, newWorkspaceId })
}

// Provider commands
export async function providerList(): Promise<Provider[]> {
return invoke<Provider[]>("provider_list")
Expand Down
54 changes: 53 additions & 1 deletion desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ClipboardCopy,
Ellipsis,
Monitor,
Pencil,
Play,
RefreshCw,
RotateCcw,
Expand All @@ -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"
Expand All @@ -38,6 +40,7 @@ import {
workspaceRebuild,
workspaceReset,
workspaceDelete,
workspaceRename,
workspaceLogsList,
workspaceLogRead,
workspaceLogDelete,
Expand Down Expand Up @@ -121,6 +124,9 @@ let connecting = $state(false)
let ideComboOpen = $state(false)
let ideSearch = $state("")
let selectedIde = $state<string | null>(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) =>
Expand Down Expand Up @@ -357,14 +363,60 @@ 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
}
}
</script>

<div class="flex min-h-0 flex-1 flex-col gap-6">
<div class="flex items-center gap-4">
<Button variant="ghost" size="sm" onclick={() => goto("/workspaces")}>
&larr; Back
</Button>
<h1 class="text-2xl font-bold">{id}</h1>
{#if renaming}
<form class="flex items-center gap-2" onsubmit={(e) => { e.preventDefault(); handleRename() }}>
<Input
data-slot="workspace-rename-input"
value={renameValue}
oninput={(e) => (renameValue = e.currentTarget.value)}
class="h-8 w-56 text-lg font-bold"
disabled={renameSaving}
/>
<Button data-slot="workspace-rename-save" variant="outline" size="sm" type="submit" disabled={renameSaving || !renameValue.trim()}>
{renameSaving ? "Saving..." : "Save"}
</Button>
<Button data-slot="workspace-rename-cancel" variant="ghost" size="sm" type="button" onclick={() => (renaming = false)} disabled={renameSaving}>
Cancel
</Button>
</form>
{:else}
<h1 class="text-2xl font-bold">{id}</h1>
<Button data-slot="workspace-rename-btn" variant="ghost" size="icon-sm" onclick={startRename} disabled={operationRunning}>
<Pencil class="h-4 w-4" />
<span class="sr-only">Rename</span>
</Button>
{/if}
{#if workspace?.provider?.name}
<span class={badgeVariants({ variant: "secondary" })}>{workspace.provider.name}</span>
{/if}
Expand Down
Loading