From cf2c1461dc26759ebee8de477d267141093b8bf1 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 28 May 2026 11:54:46 -0500 Subject: [PATCH 1/5] feat(ide): add marimo notebook IDE support Adds marimo (https://marimo.io) as a selectable browser-based IDE, modeled on the existing Jupyter Notebook integration. The agent installs marimo via pip and launches `marimo edit --headless --host 0.0.0.0 --port 10800 --no-token `, exposed to the user through the same SSH-tunneled browser-opener flow as Jupyter/RStudio. Auth is disabled (--no-token) on the same rationale as Jupyter's --NotebookApp.token='': the SSH tunnel is the security boundary. --- cmd/agent/container/setup.go | 6 + .../src/renderer/public/icons/ides/marimo.svg | 19 +++ .../public/icons/ides/marimo_dark.svg | 19 +++ .../workspace/WorkspaceWizard.svelte | 1 + .../src/pages/WorkspaceDetailPage.svelte | 1 + pkg/config/ide.go | 1 + pkg/ide/ideparse/parse.go | 9 ++ pkg/ide/marimo/marimo.go | 117 ++++++++++++++++++ pkg/ide/opener/opener.go | 19 +++ pkg/ide/types.go | 2 + 10 files changed, 194 insertions(+) create mode 100644 desktop/src/renderer/public/icons/ides/marimo.svg create mode 100644 desktop/src/renderer/public/icons/ides/marimo_dark.svg create mode 100644 pkg/ide/marimo/marimo.go diff --git a/cmd/agent/container/setup.go b/cmd/agent/container/setup.go index 9802723eb..843822df2 100644 --- a/cmd/agent/container/setup.go +++ b/cmd/agent/container/setup.go @@ -35,6 +35,7 @@ import ( "github.com/devsy-org/devsy/pkg/ide/fleet" "github.com/devsy-org/devsy/pkg/ide/jetbrains" "github.com/devsy-org/devsy/pkg/ide/jupyter" + "github.com/devsy-org/devsy/pkg/ide/marimo" "github.com/devsy-org/devsy/pkg/ide/openvscode" "github.com/devsy-org/devsy/pkg/ide/rstudio" "github.com/devsy-org/devsy/pkg/ide/vscode" @@ -617,6 +618,11 @@ func (cmd *SetupContainerCmd) installIDE( setupInfo.SubstitutionContext.ContainerWorkspaceFolder, config.GetRemoteUser(setupInfo), ide.Options). Install() + case string(config2.IDEMarimo): + return marimo.NewMarimoServer( + setupInfo.SubstitutionContext.ContainerWorkspaceFolder, + config.GetRemoteUser(setupInfo), ide.Options). + Install() case string(config2.IDERStudio): return rstudio.NewRStudioServer( setupInfo.SubstitutionContext.ContainerWorkspaceFolder, diff --git a/desktop/src/renderer/public/icons/ides/marimo.svg b/desktop/src/renderer/public/icons/ides/marimo.svg new file mode 100644 index 000000000..f28ba734b --- /dev/null +++ b/desktop/src/renderer/public/icons/ides/marimo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/renderer/public/icons/ides/marimo_dark.svg b/desktop/src/renderer/public/icons/ides/marimo_dark.svg new file mode 100644 index 000000000..116d0b793 --- /dev/null +++ b/desktop/src/renderer/public/icons/ides/marimo_dark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte index be83a2d5a..976a95368 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte @@ -93,6 +93,7 @@ const IDE_GROUPS = [ label: "Other", options: [ { value: "jupyternotebook", label: "Jupyter Notebook", iconName: "jupyter" }, + { value: "marimo", label: "marimo", iconName: "marimo" }, { value: "vscode-insiders", label: "VS Code Insiders", iconName: "vscode_insiders" }, { value: "positron", label: "Positron", iconName: "positron" }, { value: "rstudio", label: "RStudio Server", iconName: "rstudio" }, diff --git a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte index bed291b27..516b0c73f 100644 --- a/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte +++ b/desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte @@ -81,6 +81,7 @@ const IDE_OPTIONS = [ { value: "clion", label: "CLion" }, { value: "dataspell", label: "DataSpell" }, { value: "jupyternotebook", label: "Jupyter Notebook" }, + { value: "marimo", label: "marimo" }, { value: "vscode-insiders", label: "VS Code Insiders" }, { value: "positron", label: "Positron" }, { value: "rstudio", label: "RStudio Server" }, diff --git a/pkg/config/ide.go b/pkg/config/ide.go index 828ce6f09..aa5009f12 100644 --- a/pkg/config/ide.go +++ b/pkg/config/ide.go @@ -28,6 +28,7 @@ const ( IDEWindsurf IDE = "windsurf" IDEAntigravity IDE = "antigravity" IDEBob IDE = "bob" + IDEMarimo IDE = "marimo" ) type IDEGroup string diff --git a/pkg/ide/ideparse/parse.go b/pkg/ide/ideparse/parse.go index 173909fc2..317c277f7 100644 --- a/pkg/ide/ideparse/parse.go +++ b/pkg/ide/ideparse/parse.go @@ -15,6 +15,7 @@ import ( "github.com/devsy-org/devsy/pkg/ide/fleet" "github.com/devsy-org/devsy/pkg/ide/jetbrains" "github.com/devsy-org/devsy/pkg/ide/jupyter" + "github.com/devsy-org/devsy/pkg/ide/marimo" "github.com/devsy-org/devsy/pkg/ide/openvscode" "github.com/devsy-org/devsy/pkg/ide/rstudio" "github.com/devsy-org/devsy/pkg/ide/vscode" @@ -172,6 +173,14 @@ var AllowedIDEs = []AllowedIDE{ IconDark: config.WebsiteAssetsURL + "/jupyter_dark.svg", Group: config.IDEGroupOther, }, + { + Name: config.IDEMarimo, + DisplayName: "marimo", + Options: marimo.Options, + Icon: config.WebsiteAssetsURL + "/marimo.svg", + IconDark: config.WebsiteAssetsURL + "/marimo_dark.svg", + Group: config.IDEGroupOther, + }, { Name: config.IDEVSCodeInsiders, DisplayName: "VS Code Insiders", diff --git a/pkg/ide/marimo/marimo.go b/pkg/ide/marimo/marimo.go new file mode 100644 index 000000000..864f653b5 --- /dev/null +++ b/pkg/ide/marimo/marimo.go @@ -0,0 +1,117 @@ +package marimo + +import ( + "fmt" + "os/exec" + "strconv" + + "github.com/devsy-org/devsy/pkg/command" + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/ide" + "github.com/devsy-org/devsy/pkg/log" +) + +const ( + BindAddressOption = "BIND_ADDRESS" +) + +const DefaultServerPort = 10800 + +var Options = ide.Options{ + BindAddressOption: { + Name: BindAddressOption, + Description: "The address to bind the server to locally, e.g. 0.0.0.0:12345", + Default: "", + }, +} + +type MarimoServer struct { + values map[string]config.OptionValue + workspaceFolder string + userName string +} + +func NewMarimoServer( + workspaceFolder string, + userName string, + values map[string]config.OptionValue, +) *MarimoServer { + return &MarimoServer{ + values: values, + workspaceFolder: workspaceFolder, + userName: userName, + } +} + +func (o *MarimoServer) Install() error { + if err := o.installMarimo(); err != nil { + return err + } + return o.Start() +} + +func (o *MarimoServer) Start() error { + return command.StartBackgroundOnce("marimo", func() (*exec.Cmd, error) { + log.Infof("Starting marimo in background...") + runCommand := fmt.Sprintf( + "marimo edit --headless --host 0.0.0.0 --port %s --no-token '%s'", + strconv.Itoa(DefaultServerPort), + o.workspaceFolder, + ) + args := []string{} + if o.userName != "" { + args = append(args, "su", o.userName, "-w", "SSH_AUTH_SOCK", "-l", "-c", runCommand) + } else { + args = append(args, "sh", "-l", "-c", runCommand) + } + //nolint:gosec // args are constructed from trusted inputs + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = o.workspaceFolder + return cmd, nil + }) +} + +func (o *MarimoServer) installMarimo() error { + if command.ExistsForUser("marimo", o.userName) { + return nil + } + + baseCommand := pickPip(o.userName) + if baseCommand == "" { + return fmt.Errorf( + "seems like neither pip3 nor pip exists, please make sure to install python correctly", + ) + } + + runCommand := fmt.Sprintf("%s install marimo", baseCommand) + args := []string{} + if o.userName != "" { + args = append(args, "su", o.userName, "-c", runCommand) + } else { + args = append(args, "sh", "-c", runCommand) + } + + log.Infof("installing marimo") + //nolint:gosec // args are constructed from trusted inputs + out, err := exec.Command(args[0], args[1:]...).CombinedOutput() + if err != nil { + return fmt.Errorf( + "error installing marimo: %w", + command.WrapCommandError(out, err), + ) + } + + log.Info("installed marimo") + return nil +} + +func pickPip(userName string) string { + switch { + case command.ExistsForUser("pip3", userName): + return "pip3" + case command.ExistsForUser("pip", userName): + return "pip" + default: + return "" + } +} diff --git a/pkg/ide/opener/opener.go b/pkg/ide/opener/opener.go index 582c40dd2..9c353fdae 100644 --- a/pkg/ide/opener/opener.go +++ b/pkg/ide/opener/opener.go @@ -18,6 +18,7 @@ import ( "github.com/devsy-org/devsy/pkg/ide/fleet" "github.com/devsy-org/devsy/pkg/ide/jetbrains" "github.com/devsy-org/devsy/pkg/ide/jupyter" + "github.com/devsy-org/devsy/pkg/ide/marimo" "github.com/devsy-org/devsy/pkg/ide/openvscode" "github.com/devsy-org/devsy/pkg/ide/rstudio" "github.com/devsy-org/devsy/pkg/ide/vscode" @@ -80,6 +81,8 @@ func browserIDEOpener( return openCodeServerBrowser, true case string(config.IDEJupyterNotebook): return openJupyterBrowser, true + case string(config.IDEMarimo): + return openMarimoBrowser, true case string(config.IDERStudio): return openRStudioBrowser, true default: @@ -347,6 +350,22 @@ func openJupyterBrowser( }) } +func openMarimoBrowser( + ctx context.Context, + ideOptions map[string]config.OptionValue, + params IDEParams, +) (string, error) { + return openBrowserIDE(ctx, params, browserIDESpec{ + BindAddrOption: marimo.Options.GetValue(ideOptions, marimo.BindAddressOption), + DefaultPort: marimo.DefaultServerPort, + Label: "marimo", + LogName: "marimo", + TargetURLFn: func(port int, _ string) string { + return fmt.Sprintf("http://localhost:%d/", port) + }, + }) +} + func openRStudioBrowser( ctx context.Context, ideOptions map[string]config.OptionValue, diff --git a/pkg/ide/types.go b/pkg/ide/types.go index 3e35404ff..34db194be 100644 --- a/pkg/ide/types.go +++ b/pkg/ide/types.go @@ -55,6 +55,8 @@ func ReusesAuthSock(ide string) bool { return true case string(config.IDEJupyterNotebook): return true + case string(config.IDEMarimo): + return true default: return false } From fac65350154ff9925617437db8fc96eb03eb6404 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 28 May 2026 11:58:25 -0500 Subject: [PATCH 2/5] refactor(ide): prefer uv when installing Python-based IDEs Extracts a shared ide.PythonInstallCommand helper that returns the install command in this preference order: uv pip install --system pip3 install pip install Used by both the jupyter and marimo installers. uv is dramatically faster than pip and is increasingly common in dev containers; falling back to pip preserves behavior on images that don't ship uv. Also reorders jupyter.go to satisfy funcorder and wraps its two exec.Command calls with the same gosec nolint comment used in marimo.go, since touching the file re-triggered the diff-only pre-commit lint. --- pkg/ide/jupyter/jupyter.go | 78 +++++++++++++++++--------------------- pkg/ide/marimo/marimo.go | 20 ++-------- pkg/ide/types.go | 17 +++++++++ 3 files changed, 54 insertions(+), 61 deletions(-) diff --git a/pkg/ide/jupyter/jupyter.go b/pkg/ide/jupyter/jupyter.go index d346a593b..2a849f68f 100644 --- a/pkg/ide/jupyter/jupyter.go +++ b/pkg/ide/jupyter/jupyter.go @@ -15,6 +15,8 @@ const ( BindAddressOption = "BIND_ADDRESS" ) +const DefaultServerPort = 10700 + var Options = ide.Options{ BindAddressOption: { Name: BindAddressOption, @@ -23,7 +25,11 @@ var Options = ide.Options{ }, } -const DefaultServerPort = 10700 +type JupyterNotbookServer struct { + values map[string]config.OptionValue + workspaceFolder string + userName string +} func NewJupyterNotebookServer( workspaceFolder string, @@ -37,40 +43,45 @@ func NewJupyterNotebookServer( } } -type JupyterNotbookServer struct { - values map[string]config.OptionValue - workspaceFolder string - userName string -} - func (o *JupyterNotbookServer) Install() error { - err := o.installNotebook() - if err != nil { + if err := o.installNotebook(); err != nil { return err } - return o.Start() } +func (o *JupyterNotbookServer) Start() error { + return command.StartBackgroundOnce("jupyter", func() (*exec.Cmd, error) { + log.Infof("Starting jupyter notebook in background...") + runCommand := fmt.Sprintf( + "jupyter notebook --ip='*' --NotebookApp.notebook_dir='%s' --NotebookApp.token='' "+ + "--NotebookApp.password='' --no-browser --port '%s' --allow-root", + o.workspaceFolder, + strconv.Itoa(DefaultServerPort), + ) + args := []string{} + if o.userName != "" { + args = append(args, "su", o.userName, "-w", "SSH_AUTH_SOCK", "-l", "-c", runCommand) + } else { + args = append(args, "sh", "-l", "-c", runCommand) + } + //nolint:gosec // args are constructed from trusted inputs + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = o.workspaceFolder + return cmd, nil + }) +} + func (o *JupyterNotbookServer) installNotebook() error { if command.ExistsForUser("jupyter", o.userName) { return nil } - // check if pip3 exists - baseCommand := "" - if command.ExistsForUser("pip3", o.userName) { - baseCommand = "pip3" - } else if command.ExistsForUser("pip", o.userName) { - baseCommand = "pip" - } else { - return fmt.Errorf( - "seems like neither pip3 nor pip exists, please make sure to install python correctly", - ) + runCommand, err := ide.PythonInstallCommand(o.userName, "notebook") + if err != nil { + return err } - // install notebook command - runCommand := fmt.Sprintf("%s install notebook", baseCommand) args := []string{} if o.userName != "" { args = append(args, "su", o.userName, "-c", runCommand) @@ -78,8 +89,8 @@ func (o *JupyterNotbookServer) installNotebook() error { args = append(args, "sh", "-c", runCommand) } - // install log.Infof("installing jupyter notebook") + //nolint:gosec // args are constructed from trusted inputs out, err := exec.Command(args[0], args[1:]...).CombinedOutput() if err != nil { return fmt.Errorf( @@ -91,24 +102,3 @@ func (o *JupyterNotbookServer) installNotebook() error { log.Info("installed jupyter notebook") return nil } - -func (o *JupyterNotbookServer) Start() error { - return command.StartBackgroundOnce("jupyter", func() (*exec.Cmd, error) { - log.Infof("Starting jupyter notebook in background...") - runCommand := fmt.Sprintf( - "jupyter notebook --ip='*' --NotebookApp.notebook_dir='%s' --NotebookApp.token='' "+ - "--NotebookApp.password='' --no-browser --port '%s' --allow-root", - o.workspaceFolder, - strconv.Itoa(DefaultServerPort), - ) - args := []string{} - if o.userName != "" { - args = append(args, "su", o.userName, "-w", "SSH_AUTH_SOCK", "-l", "-c", runCommand) - } else { - args = append(args, "sh", "-l", "-c", runCommand) - } - cmd := exec.Command(args[0], args[1:]...) - cmd.Dir = o.workspaceFolder - return cmd, nil - }) -} diff --git a/pkg/ide/marimo/marimo.go b/pkg/ide/marimo/marimo.go index 864f653b5..eb86c92f7 100644 --- a/pkg/ide/marimo/marimo.go +++ b/pkg/ide/marimo/marimo.go @@ -76,14 +76,11 @@ func (o *MarimoServer) installMarimo() error { return nil } - baseCommand := pickPip(o.userName) - if baseCommand == "" { - return fmt.Errorf( - "seems like neither pip3 nor pip exists, please make sure to install python correctly", - ) + runCommand, err := ide.PythonInstallCommand(o.userName, "marimo") + if err != nil { + return err } - runCommand := fmt.Sprintf("%s install marimo", baseCommand) args := []string{} if o.userName != "" { args = append(args, "su", o.userName, "-c", runCommand) @@ -104,14 +101,3 @@ func (o *MarimoServer) installMarimo() error { log.Info("installed marimo") return nil } - -func pickPip(userName string) string { - switch { - case command.ExistsForUser("pip3", userName): - return "pip3" - case command.ExistsForUser("pip", userName): - return "pip" - default: - return "" - } -} diff --git a/pkg/ide/types.go b/pkg/ide/types.go index 34db194be..14804122e 100644 --- a/pkg/ide/types.go +++ b/pkg/ide/types.go @@ -1,13 +1,30 @@ package ide import ( + "fmt" "io" "time" + "github.com/devsy-org/devsy/pkg/command" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/log" ) +// PythonInstallCommand returns the shell command that installs a Python package +// for the given user, preferring uv when present and falling back to pip3 then +// pip. Returns an error if none are available. +func PythonInstallCommand(userName, pkg string) (string, error) { + switch { + case command.ExistsForUser("uv", userName): + return fmt.Sprintf("uv pip install --system %s", pkg), nil + case command.ExistsForUser("pip3", userName): + return fmt.Sprintf("pip3 install %s", pkg), nil + case command.ExistsForUser("pip", userName): + return fmt.Sprintf("pip install %s", pkg), nil + } + return "", fmt.Errorf("none of uv, pip3, or pip exist; install Python tooling first") +} + type IDE interface { Install() error } From 0970d8186403a0944aa04795cc01a8ecd2e7ceba Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 28 May 2026 12:33:53 -0500 Subject: [PATCH 3/5] feat(ide): add FORWARD_PORTS option to marimo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit marimo notebooks commonly embed subprocess apps (FastAPI, Streamlit, etc.) on additional ports. Expose the same FORWARD_PORTS option that code-server and openvscode use so the SSH tunnel auto-forwards any in-container listeners through to the host. Default 'true', matching the VS Code-derived browser IDEs. jupyter and rstudio still omit this — their precedent stands. --- pkg/ide/marimo/marimo.go | 9 ++++++++- pkg/ide/opener/opener.go | 7 +++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/ide/marimo/marimo.go b/pkg/ide/marimo/marimo.go index eb86c92f7..7528b390a 100644 --- a/pkg/ide/marimo/marimo.go +++ b/pkg/ide/marimo/marimo.go @@ -12,12 +12,19 @@ import ( ) const ( - BindAddressOption = "BIND_ADDRESS" + BindAddressOption = "BIND_ADDRESS" + ForwardPortsOption = "FORWARD_PORTS" ) const DefaultServerPort = 10800 var Options = ide.Options{ + ForwardPortsOption: { + Name: ForwardPortsOption, + Description: "If Devsy should automatically do port-forwarding", + Default: "true", + Enum: []string{"true", "false"}, + }, BindAddressOption: { Name: BindAddressOption, Description: "The address to bind the server to locally, e.g. 0.0.0.0:12345", diff --git a/pkg/ide/opener/opener.go b/pkg/ide/opener/opener.go index 9c353fdae..c971b17ce 100644 --- a/pkg/ide/opener/opener.go +++ b/pkg/ide/opener/opener.go @@ -358,8 +358,11 @@ func openMarimoBrowser( return openBrowserIDE(ctx, params, browserIDESpec{ BindAddrOption: marimo.Options.GetValue(ideOptions, marimo.BindAddressOption), DefaultPort: marimo.DefaultServerPort, - Label: "marimo", - LogName: "marimo", + ForwardPorts: marimo.Options.GetValue( + ideOptions, marimo.ForwardPortsOption, + ) == config.BoolTrue, + Label: "marimo", + LogName: "marimo", TargetURLFn: func(port int, _ string) string { return fmt.Sprintf("http://localhost:%d/", port) }, From ec3f51791ee0175b8adcd7413ee7d866e320b796 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 28 May 2026 14:33:57 -0500 Subject: [PATCH 4/5] style(ide): drop trailing ellipses from background-start log messages --- pkg/ide/jupyter/jupyter.go | 2 +- pkg/ide/marimo/marimo.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/ide/jupyter/jupyter.go b/pkg/ide/jupyter/jupyter.go index 2a849f68f..0c721c039 100644 --- a/pkg/ide/jupyter/jupyter.go +++ b/pkg/ide/jupyter/jupyter.go @@ -52,7 +52,7 @@ func (o *JupyterNotbookServer) Install() error { func (o *JupyterNotbookServer) Start() error { return command.StartBackgroundOnce("jupyter", func() (*exec.Cmd, error) { - log.Infof("Starting jupyter notebook in background...") + log.Infof("Starting jupyter notebook in background") runCommand := fmt.Sprintf( "jupyter notebook --ip='*' --NotebookApp.notebook_dir='%s' --NotebookApp.token='' "+ "--NotebookApp.password='' --no-browser --port '%s' --allow-root", diff --git a/pkg/ide/marimo/marimo.go b/pkg/ide/marimo/marimo.go index 7528b390a..fb6e24680 100644 --- a/pkg/ide/marimo/marimo.go +++ b/pkg/ide/marimo/marimo.go @@ -59,7 +59,7 @@ func (o *MarimoServer) Install() error { func (o *MarimoServer) Start() error { return command.StartBackgroundOnce("marimo", func() (*exec.Cmd, error) { - log.Infof("Starting marimo in background...") + log.Infof("Starting marimo in background") runCommand := fmt.Sprintf( "marimo edit --headless --host 0.0.0.0 --port %s --no-token '%s'", strconv.Itoa(DefaultServerPort), From 0b45d551e6f6e4911e833146557c88efb34cc3dd Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 28 May 2026 15:45:38 -0500 Subject: [PATCH 5/5] refactor(ide): use 'uv tool install' instead of 'uv pip install --system' uv tool install is the uv-native path for CLI tools: creates an isolated env per tool, drops a shim in ~/.local/bin, doesn't touch system Python, and sidesteps PEP 668 externally-managed-environment errors. The pip3/pip fallbacks keep existing behavior. Also adds 'marimo' to IDE_ICON_DARK_VARIANTS in the wizard so the restored marimo_dark.svg actually loads in dark mode. --- .../src/lib/components/workspace/WorkspaceWizard.svelte | 1 + pkg/ide/types.go | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte index 976a95368..da4b5e759 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte @@ -48,6 +48,7 @@ const IDE_ICON_DARK_VARIANTS = new Set([ "code-server", "cursor", "jupyter", + "marimo", "none", "zed", ]) diff --git a/pkg/ide/types.go b/pkg/ide/types.go index 14804122e..8d9bd478c 100644 --- a/pkg/ide/types.go +++ b/pkg/ide/types.go @@ -13,10 +13,15 @@ import ( // PythonInstallCommand returns the shell command that installs a Python package // for the given user, preferring uv when present and falling back to pip3 then // pip. Returns an error if none are available. +// +// When uv is used, the package is installed as a uv-managed tool (isolated +// env, shim binary placed in ~/.local/bin); this avoids touching system +// Python and sidesteps PEP 668 externally-managed-environment errors. The +// pip fallbacks preserve the existing system-install behavior. func PythonInstallCommand(userName, pkg string) (string, error) { switch { case command.ExistsForUser("uv", userName): - return fmt.Sprintf("uv pip install --system %s", pkg), nil + return fmt.Sprintf("uv tool install %s", pkg), nil case command.ExistsForUser("pip3", userName): return fmt.Sprintf("pip3 install %s", pkg), nil case command.ExistsForUser("pip", userName):