From de3b9ee9b9d6f28f78e68f83b3aa23eb18528706 Mon Sep 17 00:00:00 2001
From: minhn4
Date: Sun, 16 Aug 2026 20:15:25 +0700
Subject: [PATCH 1/3] feat(app): say why Work opens the Add sandbox dialog
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Clicking Work without a sandbox opens the create dialog, because Work with no
sandbox is a dead end. That dialog arrives unbidden and said nothing about why
it was there, so the first thing it asked for was a name for something the
user had not decided they wanted.
It now leads with the reason, and says what cancelling does — which is the part
that is not guessable: the side stays on Code rather than switching to an empty
Work.
Only the gate passes the flag. Settings is a deliberate "add a sandbox" and
needs no preamble, and the Work empty-state button sits directly beneath the
same explanation already.
Co-Authored-By: Claude Opus 5 (1M context)
---
CHANGELOG.md | 6 ++++++
packages/app/src/i18n/en.ts | 2 ++
packages/app/src/i18n/es.ts | 2 ++
packages/app/src/sandbox/dialog-add.tsx | 11 ++++++++++-
packages/app/src/workspace/tabs.tsx | 2 +-
5 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0195b7a7d3..a207e340ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@ file is the human-readable summary of what changed and why.
### Desktop / Web UI
+- **Switching to Work with no sandbox explains itself** — clicking **Work**
+ before you have a sandbox opens the Add sandbox dialog, which was a dialog
+ you did not ask for with nothing to say about why it appeared. It now leads
+ with the reason: Work runs each session inside a sandbox, and cancelling
+ leaves you on Code. Reaching the same dialog deliberately, from Settings,
+ is unchanged.
- **The CPU and memory fields explain themselves** — the Add sandbox dialog
offered two unlabelled optional boxes with no hint of what a sandbox gets if
you leave them empty, and the memory box ran past the edge of the dialog.
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 49ea12586b..ae3310cdb1 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -379,6 +379,8 @@ export const dict = {
"workspace.empty.none": "No sandbox yet. Create one to work in an isolated environment.",
"workspace.empty.starting": "Your sandbox is not running yet. It appears here once it is ready.",
"workspace.empty.stopped": "Your sandbox is stopped. Start it again from Settings → Servers.",
+ "sandbox.add.workIntro":
+ "Work runs each session inside a sandbox, so it needs one before you can use it. Cancel to stay on Code.",
"sandbox.server.add": "Add sandbox",
"sandbox.server.label": "Sandbox",
"sandbox.server.menu.label": "Sandbox",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index d064e3f931..2798dde8a5 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -370,6 +370,8 @@ export const dict = {
"workspace.empty.none": "Aún no hay ningún sandbox. Crea uno para trabajar en un entorno aislado.",
"workspace.empty.starting": "Tu sandbox todavía no está en marcha. Aparecerá aquí cuando esté listo.",
"workspace.empty.stopped": "Tu sandbox está detenido. Vuelve a iniciarlo desde Ajustes → Servidores.",
+ "sandbox.add.workIntro":
+ "Trabajo ejecuta cada sesi\u00f3n dentro de un sandbox, as\u00ed que necesitas uno para poder usarlo. Cancela para quedarte en C\u00f3digo.",
"sandbox.server.add": "Añadir sandbox",
"sandbox.server.label": "Sandbox",
"sandbox.server.menu.label": "Sandbox",
diff --git a/packages/app/src/sandbox/dialog-add.tsx b/packages/app/src/sandbox/dialog-add.tsx
index 80ea641738..1935baa14c 100644
--- a/packages/app/src/sandbox/dialog-add.tsx
+++ b/packages/app/src/sandbox/dialog-add.tsx
@@ -25,7 +25,13 @@ import {
// need to react to a creation must use it rather than watching the sandbox
// list: that list arrives over a separate IPC event, so it is still empty for
// a beat after add() resolves.
-export function DialogAddSandbox(props: { onCreated?: (config: SandboxConfig) => void }) {
+export function DialogAddSandbox(props: {
+ onCreated?: (config: SandboxConfig) => void
+ // Set when the dialog opened because Work needs a sandbox and there is none,
+ // rather than because the user asked to add one. That dialog arrives
+ // unbidden, so it has to say why it is there.
+ requiredForWork?: boolean
+}) {
const language = useLanguage()
const platform = usePlatform()
const dialog = useDialog()
@@ -197,6 +203,9 @@ export function DialogAddSandbox(props: { onCreated?: (config: SandboxConfig) =>
+
+ {language.t("sandbox.add.workIntro")}
+
{unsupportedMessage()}
}
diff --git a/packages/app/src/workspace/tabs.tsx b/packages/app/src/workspace/tabs.tsx
index 446db5a5dc..dbd3b926e6 100644
--- a/packages/app/src/workspace/tabs.tsx
+++ b/packages/app/src/workspace/tabs.tsx
@@ -82,7 +82,7 @@ export function useWorkspaceSwitch() {
// list arrives on a separate IPC event and is still empty for a beat
// after the create resolves, so checking it here left the user on
// Code until they toggled the tabs by hand.
- void dialog.show(() => enter("work")} />)
+ void dialog.show(() => enter("work")} />)
})
return
}
From b4b6671eadf704d2d4f200c443e98ef067dc7145 Mon Sep 17 00:00:00 2001
From: minhn4
Date: Sun, 16 Aug 2026 20:21:36 +0700
Subject: [PATCH 2/3] feat(app): shorten the Work sandbox explanation to one
line
Two sentences to say one thing. The second told the user what Cancel does,
which the Cancel button and the tab they came from already imply.
Co-Authored-By: Claude Opus 5 (1M context)
---
CHANGELOG.md | 5 ++---
packages/app/src/i18n/en.ts | 3 +--
packages/app/src/i18n/es.ts | 3 +--
3 files changed, 4 insertions(+), 7 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a207e340ff..00bc45ceb8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,9 +12,8 @@ file is the human-readable summary of what changed and why.
- **Switching to Work with no sandbox explains itself** — clicking **Work**
before you have a sandbox opens the Add sandbox dialog, which was a dialog
you did not ask for with nothing to say about why it appeared. It now leads
- with the reason: Work runs each session inside a sandbox, and cancelling
- leaves you on Code. Reaching the same dialog deliberately, from Settings,
- is unchanged.
+ with the reason: Work requires a sandbox environment to run sessions.
+ Reaching the same dialog deliberately, from Settings, is unchanged.
- **The CPU and memory fields explain themselves** — the Add sandbox dialog
offered two unlabelled optional boxes with no hint of what a sandbox gets if
you leave them empty, and the memory box ran past the edge of the dialog.
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index ae3310cdb1..fa0b567b0d 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -379,8 +379,7 @@ export const dict = {
"workspace.empty.none": "No sandbox yet. Create one to work in an isolated environment.",
"workspace.empty.starting": "Your sandbox is not running yet. It appears here once it is ready.",
"workspace.empty.stopped": "Your sandbox is stopped. Start it again from Settings → Servers.",
- "sandbox.add.workIntro":
- "Work runs each session inside a sandbox, so it needs one before you can use it. Cancel to stay on Code.",
+ "sandbox.add.workIntro": "Work requires a sandbox environment to run sessions.",
"sandbox.server.add": "Add sandbox",
"sandbox.server.label": "Sandbox",
"sandbox.server.menu.label": "Sandbox",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index 2798dde8a5..ad1b43daa1 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -370,8 +370,7 @@ export const dict = {
"workspace.empty.none": "Aún no hay ningún sandbox. Crea uno para trabajar en un entorno aislado.",
"workspace.empty.starting": "Tu sandbox todavía no está en marcha. Aparecerá aquí cuando esté listo.",
"workspace.empty.stopped": "Tu sandbox está detenido. Vuelve a iniciarlo desde Ajustes → Servidores.",
- "sandbox.add.workIntro":
- "Trabajo ejecuta cada sesi\u00f3n dentro de un sandbox, as\u00ed que necesitas uno para poder usarlo. Cancela para quedarte en C\u00f3digo.",
+ "sandbox.add.workIntro": "Trabajo requiere un entorno de sandbox para ejecutar sesiones.",
"sandbox.server.add": "Añadir sandbox",
"sandbox.server.label": "Sandbox",
"sandbox.server.menu.label": "Sandbox",
From 4940470954dba867dbebc12e8ac163cb311cc21b Mon Sep 17 00:00:00 2001
From: minhn4
Date: Sun, 16 Aug 2026 20:23:57 +0700
Subject: [PATCH 3/3] feat(app): put the Work explanation under the dialog
title
It explains the whole dialog, not the name field it happened to sit above, so
it belongs with the title rather than as the first line of the body.
Uses the component's own DialogTitleGroup instead of placing a paragraph by
hand: that is what the description slot is for, it carries the intended
typography, and it renders a Kobalte Description, so the dialog gains
aria-describedby and screen readers announce the reason with the title.
Co-Authored-By: Claude Opus 5 (1M context)
---
packages/app/src/sandbox/dialog-add.tsx | 26 ++++++++++++++++++++-----
1 file changed, 21 insertions(+), 5 deletions(-)
diff --git a/packages/app/src/sandbox/dialog-add.tsx b/packages/app/src/sandbox/dialog-add.tsx
index 1935baa14c..9017cc7a6a 100644
--- a/packages/app/src/sandbox/dialog-add.tsx
+++ b/packages/app/src/sandbox/dialog-add.tsx
@@ -1,7 +1,14 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { showToast } from "@opencode-ai/ui/toast"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
-import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
+import {
+ Dialog,
+ DialogBody,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTitleGroup,
+} from "@opencode-ai/ui/v2/dialog-v2"
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
@@ -199,13 +206,22 @@ export function DialogAddSandbox(props: {
return (