diff --git a/packages/app/src/app/app.tsx b/packages/app/src/app/app.tsx
index 354abde1b..552c3d6c8 100644
--- a/packages/app/src/app/app.tsx
+++ b/packages/app/src/app/app.tsx
@@ -4328,6 +4328,7 @@ export default function App() {
openRenameWorkspace,
editWorkspaceConnection: openWorkspaceConnectionSettings,
forgetWorkspace: workspaceStore.forgetWorkspace,
+ stopSandbox: workspaceStore.stopSandbox,
scheduledJobs: scheduledJobs(),
scheduledJobsSource: scheduledJobsSource(),
scheduledJobsSourceReady: scheduledJobsSourceReady(),
@@ -4488,8 +4489,10 @@ export default function App() {
exportWorkspaceBusy: workspaceStore.exportingWorkspaceConfig(),
clientConnected: Boolean(client()),
openworkServerStatus: openworkServerStatus(),
+ openworkServerClient: openworkServerClient(),
openworkServerSettings: openworkServerSettings(),
openworkServerHostInfo: openworkServerHostInfo(),
+ openworkServerWorkspaceId: openworkServerWorkspaceId(),
engineInfo: workspaceStore.engine(),
stopHost,
headerStatus: headerStatus(),
@@ -4775,10 +4778,41 @@ export default function App() {
onConfirm={(preset, folder) =>
workspaceStore.createWorkspaceFlow(preset, folder)
}
- onConfirmWorker={(preset, folder) =>
- workspaceStore.createWorkerFlow(preset, folder)
+ onConfirmWorker={
+ isTauriRuntime()
+ ? (preset, folder) => workspaceStore.createSandboxFlow(preset, folder)
+ : undefined
}
- workerLabel="Create worker"
+ workerDisabled={(() => {
+ if (!isTauriRuntime()) return true;
+ const doctor = workspaceStore.sandboxDoctorResult?.();
+ return !doctor?.ready;
+ })()}
+ workerDisabledReason={(() => {
+ if (!isTauriRuntime()) return t("app.error.tauri_required", currentLocale());
+ const doctor = workspaceStore.sandboxDoctorResult?.();
+ if (doctor?.ready) return null;
+ if (workspaceStore.sandboxDoctorBusy?.()) {
+ return t("dashboard.sandbox_checking_docker", currentLocale());
+ }
+ const message = doctor?.error?.trim();
+ return message || t("dashboard.sandbox_get_ready_desc", currentLocale());
+ })()}
+ workerCtaLabel={t("dashboard.sandbox_get_ready_action", currentLocale())}
+ workerCtaDescription={t("dashboard.sandbox_get_ready_desc", currentLocale())}
+ onWorkerCta={async () => {
+ const url = "https://www.docker.com/products/docker-desktop/";
+ if (isTauriRuntime()) {
+ const { openUrl } = await import("@tauri-apps/plugin-opener");
+ await openUrl(url);
+ } else {
+ window.open(url, "_blank", "noopener,noreferrer");
+ }
+ }}
+ workerRetryLabel={t("common.retry", currentLocale())}
+ onWorkerRetry={() => {
+ void workspaceStore.refreshSandboxDoctor?.();
+ }}
submitting={busy() && busyLabel() === "status.creating_workspace"}
/>
diff --git a/packages/app/src/app/components/create-workspace-modal.tsx b/packages/app/src/app/components/create-workspace-modal.tsx
index 75866f005..14e783df0 100644
--- a/packages/app/src/app/components/create-workspace-modal.tsx
+++ b/packages/app/src/app/components/create-workspace-modal.tsx
@@ -18,6 +18,13 @@ export default function CreateWorkspaceModal(props: {
subtitle?: string;
confirmLabel?: string;
workerLabel?: string;
+ workerDisabled?: boolean;
+ workerDisabledReason?: string | null;
+ workerCtaLabel?: string;
+ workerCtaDescription?: string;
+ onWorkerCta?: () => void;
+ workerRetryLabel?: string;
+ onWorkerRetry?: () => void;
}) {
let pickFolderRef: HTMLButtonElement | undefined;
const translate = (key: string) => t(key, currentLocale());
@@ -76,10 +83,14 @@ export default function CreateWorkspaceModal(props: {
const title = () => props.title ?? translate("dashboard.create_workspace_title");
const subtitle = () => props.subtitle ?? translate("dashboard.create_workspace_subtitle");
const confirmLabel = () => props.confirmLabel ?? translate("dashboard.create_workspace_confirm");
- const workerLabel = () => props.workerLabel ?? "Create worker";
+ const workerLabel = () => props.workerLabel ?? translate("dashboard.create_sandbox_confirm");
const isInline = () => props.inline ?? false;
const submitting = () => props.submitting ?? false;
+ const workerDisabled = () => Boolean(props.workerDisabled);
+ const workerDisabledReason = () => (props.workerDisabledReason ?? "").trim();
+ const showWorkerCallout = () => Boolean(props.onConfirmWorker && workerDisabled() && workerDisabledReason());
+
const content = (
@@ -180,37 +191,63 @@ export default function CreateWorkspaceModal(props: {
-
-
-
+
+
+
+
{translate("dashboard.sandbox_get_ready_title")}
+
+
+ {props.workerCtaDescription?.trim() || workerDisabledReason()}
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
-
+
);
diff --git a/packages/app/src/app/components/session/composer.tsx b/packages/app/src/app/components/session/composer.tsx
index db713a322..086616f60 100644
--- a/packages/app/src/app/components/session/composer.tsx
+++ b/packages/app/src/app/components/session/composer.tsx
@@ -46,6 +46,8 @@ type ComposerProps = {
recentFiles: string[];
searchFiles: (query: string) => Promise;
isRemoteWorkspace: boolean;
+ isSandboxWorkspace: boolean;
+ onUploadInboxFiles?: (files: File[]) => void | Promise;
attachmentsEnabled: boolean;
attachmentsDisabledReason: string | null;
listCommands: () => Promise;
@@ -367,6 +369,7 @@ const buildRangeFromOffsets = (root: HTMLElement, start: number, end: number) =>
export default function Composer(props: ComposerProps) {
let editorRef: HTMLDivElement | undefined;
let fileInputRef: HTMLInputElement | undefined;
+ let inboxFileInputRef: HTMLInputElement | undefined;
let variantPickerRef: HTMLDivElement | undefined;
let mentionSearchRun = 0;
let suppressPromptSync = false;
@@ -387,6 +390,7 @@ export default function Composer(props: ComposerProps) {
const [historyIndex, setHistoryIndex] = createSignal({ prompt: -1, shell: -1 });
const [history, setHistory] = createSignal({ prompt: [] as ComposerDraft[], shell: [] as ComposerDraft[] });
const [variantMenuOpen, setVariantMenuOpen] = createSignal(false);
+ const [showInboxUploadAction, setShowInboxUploadAction] = createSignal(false);
const activeVariant = createMemo(() => props.modelVariant ?? "none");
const attachmentsDisabled = createMemo(() => !props.attachmentsEnabled);
@@ -466,7 +470,7 @@ export default function Composer(props: ComposerProps) {
}));
const all = [...agents, ...recentFiles, ...searchFiles];
const list = query
- ? fuzzysort.go(query, all, { keys: ["display"] }).map((entry) => entry.obj)
+ ? fuzzysort.go(query, all, { keys: ["display"] }).map((entry: any) => entry.obj)
: all;
const groups: MentionGroup[] = [];
const bucket = new Map();
@@ -619,7 +623,7 @@ export default function Composer(props: ComposerProps) {
if (!query) return commands.slice(0, 15);
return fuzzysort
.go(query, commands, { keys: ["name", "description"] })
- .map((entry) => entry.obj)
+ .map((entry: any) => entry.obj)
.slice(0, 15);
});
@@ -929,7 +933,6 @@ export default function Composer(props: ComposerProps) {
.map((item) => item.getAsFile())
.filter((file): file is File => !!file);
const allFiles = files.length ? files : itemFiles;
-
if (allFiles.length) {
event.preventDefault();
const hasSupported = allFiles.some((file) => ACCEPTED_FILE_TYPES.includes(file.type));
@@ -941,6 +944,20 @@ export default function Composer(props: ComposerProps) {
return;
}
+ const plainForCheck = clipboard.getData("text/plain") ?? "";
+ const trimmedForCheck = plainForCheck.trim();
+ if (trimmedForCheck && props.isSandboxWorkspace) {
+ const hasFileUrl = /file:\/\//i.test(trimmedForCheck);
+ const hasAbsolutePosix = /(^|\s)\/(Users|home|var|etc|opt|tmp|private|Volumes|Applications)\//.test(trimmedForCheck);
+ const hasAbsoluteWindows = /(^|\s)[a-zA-Z]:\\/.test(trimmedForCheck);
+ if (hasFileUrl || hasAbsolutePosix || hasAbsoluteWindows) {
+ props.onToast(
+ "Sandboxes can't access local file paths. Upload the file to the workspace inbox instead."
+ );
+ setShowInboxUploadAction(Boolean(props.onUploadInboxFiles));
+ }
+ }
+
const plain = clipboard.getData("text/plain") || clipboard.getData("text") || "";
const html = clipboard.getData("text/html") || "";
const raw = plain || (html ? htmlToPlainText(html) : "");
@@ -960,6 +977,12 @@ export default function Composer(props: ComposerProps) {
emitDraftChange();
};
+ createEffect(() => {
+ if (!props.toast) {
+ setShowInboxUploadAction(false);
+ }
+ });
+
const handleDrop = (event: DragEvent) => {
if (!event.dataTransfer) return;
event.preventDefault();
@@ -1395,7 +1418,18 @@ export default function Composer(props: ComposerProps) {
- {props.toast}
+
+ {props.toast}
+
+
+
+
@@ -1429,6 +1463,20 @@ export default function Composer(props: ComposerProps) {
+ {
+ const target = event.currentTarget as HTMLInputElement;
+ const files = Array.from(target.files ?? []);
+ if (files.length && props.onUploadInboxFiles) {
+ void Promise.resolve(props.onUploadInboxFiles(files));
+ }
+ target.value = "";
+ }}
+ />
void;
onTestWorkspaceConnection: (workspaceId: string) => void;
onForgetWorkspace: (workspaceId: string) => void;
+ onStopSandbox?: (workspaceId: string) => void;
onReorderWorkspace: (fromId: string, toId: string | null) => void;
onSelectSession: (workspaceId: string, sessionId: string) => void;
selectedSessionId: string | null;
@@ -359,7 +360,7 @@ export default function SessionSidebar(props: SidebarProps) {
- Remote
+ {group.workspace.sandboxContainerName?.trim() ? "Sandbox" : "Remote"}
@@ -439,6 +440,17 @@ export default function SessionSidebar(props: SidebarProps) {
Test connection
+
+
+
+
+
+