From 5a4d1d72790d543c88870561dc6fe81516449926 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 27 May 2026 15:57:41 -0500 Subject: [PATCH 1/4] fix(ide): honor freshly selected IDE on workspace start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop Start button passed no --ide flag to `devsy up`, so the CLI fell back to the persisted workspace IDE in workspace.json. Switching the dropdown after the workspace had already been created with a different IDE had no effect — the old IDE launched anyway. - Pass the current dropdown selection from handleStart to workspaceUp alongside the workspace folder. - Add `devsy ide set ` which persists the IDE via ideparse.RefreshIDEOptions without spinning up the container. - Wire the desktop IDE dropdown's onSelect to call the new command so the selection persists immediately. Roll back the UI state on failure and skip the call for the "none" sentinel. --- cmd/ide/ide.go | 1 + cmd/ide/set.go | 78 +++++++++++++++++++ desktop/src/main/ipc.ts | 8 ++ desktop/src/renderer/src/lib/ipc/commands.ts | 7 ++ .../src/pages/WorkspaceDetailPage.svelte | 20 ++++- 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 cmd/ide/set.go diff --git a/cmd/ide/ide.go b/cmd/ide/ide.go index 2ae6ffd4c..d2621f5e1 100644 --- a/cmd/ide/ide.go +++ b/cmd/ide/ide.go @@ -13,6 +13,7 @@ func NewIDECmd(flags *flags.GlobalFlags) *cobra.Command { } ideCmd.AddCommand(NewUseCmd(flags)) + ideCmd.AddCommand(NewSetCmd(flags)) ideCmd.AddCommand(NewSetOptionsCmd(flags)) ideCmd.AddCommand(NewOptionsCmd(flags)) ideCmd.AddCommand(NewListCmd(flags)) diff --git a/cmd/ide/set.go b/cmd/ide/set.go new file mode 100644 index 000000000..2d5db7997 --- /dev/null +++ b/cmd/ide/set.go @@ -0,0 +1,78 @@ +package ide + +import ( + "context" + "fmt" + "strings" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/ide/ideparse" + "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/spf13/cobra" +) + +// SetCmd holds the set cmd flags. +type SetCmd struct { + *flags.GlobalFlags + + Options []string +} + +// NewSetCmd creates a command that sets the IDE for an existing workspace +// without starting the workspace. +func NewSetCmd(flags *flags.GlobalFlags) *cobra.Command { + cmd := &SetCmd{ + GlobalFlags: flags, + } + setCmd := &cobra.Command{ + Use: "set [workspace] [ide]", + Short: "Set the IDE for an existing workspace without starting it", + Long: `Set the IDE for an existing workspace without starting it. + +The change is persisted to the workspace config and will be used on the next +'devsy up'. Available IDEs can be listed with 'devsy ide list'.`, + RunE: func(cobraCmd *cobra.Command, args []string) error { + if len(args) != 2 { + return fmt.Errorf("usage: devsy ide set ") + } + return cmd.Run(cobraCmd.Context(), args[0], args[1]) + }, + } + + setCmd.Flags(). + StringArrayVarP(&cmd.Options, "option", "o", []string{}, "IDE option in the form KEY=VALUE") + return setCmd +} + +// Run runs the command logic. +func (cmd *SetCmd) Run(_ context.Context, workspaceID, ideName string) error { + devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider) + if err != nil { + return err + } + + contextName := devsyConfig.DefaultContext + if !provider.WorkspaceExists(contextName, workspaceID) { + return fmt.Errorf("workspace %q not found in context %q", workspaceID, contextName) + } + + workspace, err := provider.LoadWorkspaceConfig(contextName, workspaceID) + if err != nil { + return fmt.Errorf("load workspace config: %w", err) + } + + ideName = strings.ToLower(ideName) + if _, err := ideparse.GetIDEOptions(ideName); err != nil { + return err + } + + workspace, err = ideparse.RefreshIDEOptions(devsyConfig, workspace, ideName, cmd.Options) + if err != nil { + return fmt.Errorf("refresh ide options: %w", err) + } + + log.Infof("set IDE for workspace %q to %q", workspace.ID, workspace.IDE.Name) + return nil +} diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index c8905b9ab..72a797207 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -79,6 +79,14 @@ export function registerIpcHandlers(deps: IpcDependencies): { tunnelProcesses: M }, ) + ipcMain.handle( + "workspace_set_ide", + async (_event, args: { workspaceId: string; ide: string }) => { + trackEvent("workspace_set_ide", { ide: args.ide }) + await cli.runRaw(["ide", "set", args.workspaceId, args.ide]) + }, + ) + // ── 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 fe77565a0..5c13a7b8f 100644 --- a/desktop/src/renderer/src/lib/ipc/commands.ts +++ b/desktop/src/renderer/src/lib/ipc/commands.ts @@ -67,6 +67,13 @@ export async function workspaceRename( return invoke("workspace_rename", { workspaceId, newWorkspaceId }) } +export async function workspaceSetIde( + workspaceId: string, + ide: string, +): Promise { + return invoke("workspace_set_ide", { workspaceId, ide }) +} + // Provider commands export async function providerList(): Promise { return invoke("provider_list") diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index 332f8f3fd..334627940 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -41,6 +41,7 @@ import { workspaceReset, workspaceDelete, workspaceRename, + workspaceSetIde, workspaceLogsList, workspaceLogRead, workspaceLogDelete, @@ -305,9 +306,16 @@ function startStreamingOp(label: string) { } async function handleStart() { + const ide = currentIde !== "none" ? currentIde : undefined + const folder = customFolder || undefined startStreamingOp("Start") try { - commandId = await workspaceUp({ source: id, debug: isDebug() }) + commandId = await workspaceUp({ + source: id, + ide, + debug: isDebug(), + workspaceFolder: folder, + }) } catch (err) { operationRunning = false toasts.error(`Failed to start: ${extractErrorMessage(err)}`) @@ -545,10 +553,18 @@ async function handleRename() { { + onSelect={async () => { + const prev = selectedIde selectedIde = ide.value ideComboOpen = false ideSearch = "" + if (ide.value === "none") return + try { + await workspaceSetIde(id, ide.value) + } catch (err) { + selectedIde = prev + toasts.error(`Failed to set IDE: ${extractErrorMessage(err)}`) + } }} > From 317c13e02ab24a83c43d2ff7f1bc500689261dc1 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 27 May 2026 16:37:40 -0500 Subject: [PATCH 2/4] fix(ide): guard IDE-set rollback against out-of-order responses --- desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index 334627940..781712003 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -125,6 +125,7 @@ let connecting = $state(false) let ideComboOpen = $state(false) let ideSearch = $state("") let selectedIde = $state(null) +let ideSetSeq = 0 let renaming = $state(false) let renameValue = $state("") let renameSaving = $state(false) @@ -554,6 +555,7 @@ async function handleRename() { value={ide.value} class="justify-start" onSelect={async () => { + const seq = ++ideSetSeq const prev = selectedIde selectedIde = ide.value ideComboOpen = false @@ -562,6 +564,7 @@ async function handleRename() { try { await workspaceSetIde(id, ide.value) } catch (err) { + if (seq !== ideSetSeq) return selectedIde = prev toasts.error(`Failed to set IDE: ${extractErrorMessage(err)}`) } From ab6c04833b8f037535b7bf220be9e4bed4e827e8 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 27 May 2026 16:45:12 -0500 Subject: [PATCH 3/4] feat(ide): support persisting 'none' as a workspace IDE choice Selecting "None" in the desktop IDE dropdown (or wizard) now persists IDE.Name="none" via 'devsy ide set', so the choice survives stop/start. Previously the renderer coerced "none" to undefined before reaching the CLI, which fell through to detect() and launched an IDE anyway. - Stop coercing "none" -> undefined in handleStart, handleOpenIde, and the wizard's workspaceUp call. - Stop early-returning on "none" in the dropdown onSelect so the choice is persisted like any other IDE. - Guard opener.Open with an IDENone short-circuit; container-side installIDE already returned nil for IDENone, but the host-side opener would have fallen into openDesktopIDE. --- .../src/lib/components/workspace/WorkspaceWizard.svelte | 2 +- desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte | 5 ++--- pkg/ide/opener/opener.go | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte index a1e338e18..c7b5f1497 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte @@ -292,7 +292,7 @@ async function handleLaunch() { source: source.trim(), workspaceId, provider: selectedProvider || undefined, - ide: selectedIde && selectedIde !== "none" ? selectedIde : undefined, + ide: selectedIde || undefined, ideLaunch: "auto", workspaceFolder: workspaceFolder.trim() || undefined, debug: true, diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index 781712003..a47c8c4fb 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -307,7 +307,7 @@ function startStreamingOp(label: string) { } async function handleStart() { - const ide = currentIde !== "none" ? currentIde : undefined + const ide = currentIde const folder = customFolder || undefined startStreamingOp("Start") try { @@ -324,7 +324,7 @@ async function handleStart() { } async function handleOpenIde() { - const ide = currentIde !== "none" ? currentIde : undefined + const ide = currentIde const folder = customFolder || undefined startStreamingOp("Open IDE") try { @@ -560,7 +560,6 @@ async function handleRename() { selectedIde = ide.value ideComboOpen = false ideSearch = "" - if (ide.value === "none") return try { await workspaceSetIde(id, ide.value) } catch (err) { diff --git a/pkg/ide/opener/opener.go b/pkg/ide/opener/opener.go index 62c32292d..b5d393562 100644 --- a/pkg/ide/opener/opener.go +++ b/pkg/ide/opener/opener.go @@ -57,6 +57,10 @@ func Open( ideOptions map[string]config.OptionValue, params IDEParams, ) (string, error) { + if ideName == string(config.IDENone) { + return "", nil + } + if fn, ok := browserIDEOpener(ideName); ok { return fn(ctx, ideOptions, params) } From a831635fe66a54656b7a5a29a768d73cc7c3a2d9 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 27 May 2026 16:52:03 -0500 Subject: [PATCH 4/4] fix(ide): disable Open IDE button when IDE is none, drop unreachable wizard guard --- .../src/lib/components/workspace/WorkspaceWizard.svelte | 2 +- desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte index c7b5f1497..bafb54ec6 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte @@ -292,7 +292,7 @@ async function handleLaunch() { source: source.trim(), workspaceId, provider: selectedProvider || undefined, - ide: selectedIde || undefined, + ide: selectedIde, ideLaunch: "auto", workspaceFolder: workspaceFolder.trim() || undefined, debug: true, diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index a47c8c4fb..a037de904 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -459,7 +459,7 @@ async function handleRename() { {/if} -