mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:38:34 +08:00
fix(frontend): make Add Workspace modal dynamic and host-aware (#306)
* fix: make Add Workspace modal dynamic and host-aware * fix: make OH_MOUNT_HOST_HOME opt-in and surface it from the modal
This commit is contained in:
@@ -25,12 +25,15 @@ If you have questions or feedback, please open a GitHub issue or join the [#proj
|
||||
### With Docker (recommended)
|
||||
|
||||
**Prerequisites**:
|
||||
|
||||
- Node.js 22.12.x or later
|
||||
- `npm`
|
||||
- Docker
|
||||
|
||||
Set `$PROJECT_PATH` to the directory on your machine where your projects live (e.g. `/path/to/your/projects`). The agent server will mount this directory so the agent can read and edit your code.
|
||||
|
||||
By default the container is kept isolated from your host home — only `~/.openhands`, `~/.claude`, `~/.codex`, and `~/.ssh` are mounted individually (and only if they exist). If you want the **Add Workspace** dialog to browse your real host filesystem, set `OH_MOUNT_HOST_HOME=1` before `npm run dev:docker` to bind-mount your entire host home onto `/home/openhands` in the container. The Add Workspace modal also shows this hint inline when it detects the mount is off.
|
||||
|
||||
```sh
|
||||
export PROJECT_PATH=/path/to/your/projects
|
||||
git clone https://github.com/OpenHands/agent-canvas.git
|
||||
@@ -51,6 +54,7 @@ especially with respect to security hardening. Notably, you can run the backend
|
||||
them from the same Agent Canvas frontend!
|
||||
|
||||
**Prerequisites**:
|
||||
|
||||
- Node.js 22.12.x or later
|
||||
- `npm`
|
||||
- `uv` (for running the agent server via `uvx`)
|
||||
|
||||
@@ -96,7 +96,11 @@ describe("WorkspaceSelectionForm", () => {
|
||||
});
|
||||
|
||||
it("Add Workspace adds only the chosen folder (not its subfolders) and dedupes on repeat", async () => {
|
||||
vi.spyOn(FilesService, "getHome").mockResolvedValue({ home: "/Users/me" });
|
||||
vi.spyOn(FilesService, "getHome").mockResolvedValue({
|
||||
home: "/Users/me",
|
||||
favorites: [],
|
||||
locations: [{ label: "/", path: "/" }],
|
||||
});
|
||||
const searchSpy = vi
|
||||
.spyOn(FilesService, "searchSubdirs")
|
||||
.mockImplementation(async (path: string) => {
|
||||
@@ -258,9 +262,10 @@ describe("WorkspaceSelectionForm", () => {
|
||||
.spyOn(FilesService, "searchSubdirs")
|
||||
.mockResolvedValue({ items: [], next_page_id: null });
|
||||
|
||||
renderForm([], [
|
||||
{ id: "custom-projects", name: "My Projects", path: "/projects" },
|
||||
]);
|
||||
renderForm(
|
||||
[],
|
||||
[{ id: "custom-projects", name: "My Projects", path: "/projects" }],
|
||||
);
|
||||
|
||||
await waitFor(() => expect(searchSpy).toHaveBeenCalledTimes(1));
|
||||
expect(searchSpy).toHaveBeenCalledWith("/projects");
|
||||
@@ -330,7 +335,11 @@ describe("WorkspaceSelectionForm", () => {
|
||||
});
|
||||
|
||||
it("Add all subdirectories saves a workspace parent and lists its children dynamically", async () => {
|
||||
vi.spyOn(FilesService, "getHome").mockResolvedValue({ home: "/Users/me" });
|
||||
vi.spyOn(FilesService, "getHome").mockResolvedValue({
|
||||
home: "/Users/me",
|
||||
favorites: [],
|
||||
locations: [{ label: "/", path: "/" }],
|
||||
});
|
||||
const searchSpy = vi
|
||||
.spyOn(FilesService, "searchSubdirs")
|
||||
.mockImplementation(async (path: string) => {
|
||||
@@ -457,4 +466,45 @@ describe("WorkspaceSelectionForm", () => {
|
||||
within(refreshedDropdown).queryByText("repoB"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Add Workspace sidebar renders backend-provided favorites dynamically and navigates into them on click", async () => {
|
||||
// Arrange: backend reports a home with a custom favorite that did NOT
|
||||
// exist in the old hardcoded list (Documents / Desktop / Downloads).
|
||||
// This is the regression guard for the original 404-on-navigate bug.
|
||||
vi.spyOn(FilesService, "getHome").mockResolvedValue({
|
||||
home: "/Users/me",
|
||||
favorites: [{ label: "projects", path: "/Users/me/projects" }],
|
||||
locations: [{ label: "/", path: "/" }],
|
||||
});
|
||||
const searchSpy = vi
|
||||
.spyOn(FilesService, "searchSubdirs")
|
||||
.mockImplementation(async (path: string) => {
|
||||
if (path === "/Users/me/projects") {
|
||||
return {
|
||||
items: [{ name: "repo1", path: "/Users/me/projects/repo1" }],
|
||||
next_page_id: null,
|
||||
};
|
||||
}
|
||||
return { items: [], next_page_id: null };
|
||||
});
|
||||
|
||||
renderForm();
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Act: open the modal and click the dynamic favorite.
|
||||
await user.click(screen.getByTestId("workspace-dropdown"));
|
||||
await user.click(await screen.findByTestId("add-workspaces-button"));
|
||||
await screen.findByTestId("folder-browser-modal");
|
||||
await user.click(
|
||||
await screen.findByTestId("folder-browser-sidebar-projects"),
|
||||
);
|
||||
|
||||
// Assert: the dynamic favorite drove the navigation, and the previously
|
||||
// hardcoded names are no longer present in the sidebar.
|
||||
await screen.findByTestId("folder-browser-entry-repo1");
|
||||
expect(searchSpy).toHaveBeenCalledWith("/Users/me/projects");
|
||||
expect(
|
||||
screen.queryByTestId("folder-browser-sidebar-documents"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+90
-17
@@ -18,6 +18,13 @@
|
||||
* - OH_AGENT_SERVER_GIT_REF: Git ref (branch/tag/SHA) of the agent-server
|
||||
* to use. Translates to the docker tag `${ref}-python`, e.g.
|
||||
* `main` -> `ghcr.io/openhands/agent-server:main-python`.
|
||||
* - OH_AGENT_SERVER_LOCAL_PATH: Absolute host path to a software-agent-sdk
|
||||
* checkout. When set, mounts the checkout at /agent-server-src inside the
|
||||
* container and reinstalls the four workspace packages
|
||||
* (openhands-{sdk,tools,workspace,agent-server}) as editable installs on
|
||||
* top of the image's pre-built venv before starting the server. Source
|
||||
* edits on the host are reflected in the running container on module
|
||||
* reload / process restart, matching the non-Docker dev loop.
|
||||
*
|
||||
* Optional credential mounts (only mounted when the host path exists):
|
||||
* - ~/.openhands -> /home/openhands/.openhands (persistence)
|
||||
@@ -25,6 +32,14 @@
|
||||
* - ~/.codex -> /home/openhands/.codex (Codex credentials)
|
||||
* - ~/.ssh -> /home/openhands/.ssh (git/ssh access)
|
||||
*
|
||||
* Optional host home mount (opt-in):
|
||||
* Set `OH_MOUNT_HOST_HOME=1` to bind-mount your entire host home onto
|
||||
* the container user's home at `/home/openhands`. This lets the
|
||||
* "Add Workspace" file browser navigate your real host filesystem
|
||||
* (and credentials/persistence dirs above are picked up automatically
|
||||
* as subpaths). Off by default so the container stays isolated from
|
||||
* the host home unless you opt in.
|
||||
*
|
||||
* Usage:
|
||||
* PROJECT_PATH=/path/to/your/projects npm run dev:docker
|
||||
* OH_AGENT_SERVER_GIT_REF=main PROJECT_PATH=... npm run dev:docker
|
||||
@@ -46,6 +61,10 @@ import {
|
||||
main,
|
||||
spawnService,
|
||||
} from "./dev-with-automation.mjs";
|
||||
import { validateLocalAgentServerPath } from "./dev-safe.mjs";
|
||||
|
||||
// Path inside the container where OH_AGENT_SERVER_LOCAL_PATH is bind-mounted.
|
||||
const CONTAINER_LOCAL_SDK_DIR = "/agent-server-src";
|
||||
|
||||
// Docker image for the agent-server.
|
||||
const AGENT_SERVER_REPO = "ghcr.io/openhands/agent-server";
|
||||
@@ -85,9 +104,7 @@ function suggestDockerless() {
|
||||
"If you'd rather not use Docker, you can run the agent-server directly with:",
|
||||
);
|
||||
logError(" npm run dev:dangerously-dockerless");
|
||||
logError(
|
||||
"Note: this runs the agent with full access to your filesystem.",
|
||||
);
|
||||
logError("Note: this runs the agent with full access to your filesystem.");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +129,9 @@ function checkDockerPrereqs(config) {
|
||||
timeout: 10_000,
|
||||
});
|
||||
if (info.status !== 0) {
|
||||
logError("docker is installed but the daemon does not appear to be running.");
|
||||
logError(
|
||||
"docker is installed but the daemon does not appear to be running.",
|
||||
);
|
||||
const stderr = info.stderr ? info.stderr.toString().trim() : "";
|
||||
if (stderr) {
|
||||
logError(` ${stderr.split("\n")[0]}`);
|
||||
@@ -134,11 +153,26 @@ function checkDockerPrereqs(config) {
|
||||
|
||||
function startAgentServerDocker(config) {
|
||||
const image = resolveAgentServerImage();
|
||||
const localSdkPath = process.env.OH_AGENT_SERVER_LOCAL_PATH;
|
||||
|
||||
// Validate up-front so we fail fast before touching docker if the user
|
||||
// pointed at a missing / incomplete checkout.
|
||||
if (localSdkPath) {
|
||||
validateLocalAgentServerPath(localSdkPath);
|
||||
}
|
||||
|
||||
logService(
|
||||
"agent-server",
|
||||
`Starting in Docker on port ${config.agentServerPort} (image: ${image})...`,
|
||||
c.blue,
|
||||
);
|
||||
if (localSdkPath) {
|
||||
logService(
|
||||
"agent-server",
|
||||
`Using local SDK source: ${localSdkPath} (mounted at ${CONTAINER_LOCAL_SDK_DIR})`,
|
||||
c.blue,
|
||||
);
|
||||
}
|
||||
|
||||
// Best-effort cleanup of any leftover container from a previous run.
|
||||
spawnSync("docker", ["rm", "-f", CONTAINER_NAME], { stdio: "ignore" });
|
||||
@@ -154,17 +188,32 @@ function startAgentServerDocker(config) {
|
||||
`${process.env.PROJECT_PATH}:/projects`,
|
||||
];
|
||||
|
||||
// Optional credential / state mounts. Only mount when the host path
|
||||
// exists so docker doesn't auto-create empty directories on the host.
|
||||
const optionalMounts = [
|
||||
[join(home, ".openhands"), "/home/openhands/.openhands"],
|
||||
[join(home, ".claude"), "/home/openhands/.claude"],
|
||||
[join(home, ".codex"), "/home/openhands/.codex"],
|
||||
[join(home, ".ssh"), "/home/openhands/.ssh"],
|
||||
];
|
||||
for (const [src, dest] of optionalMounts) {
|
||||
if (existsSync(src)) {
|
||||
dockerArgs.push("-v", `${src}:${dest}`);
|
||||
// Bind-mount the local software-agent-sdk checkout if requested. Mounted
|
||||
// rw so editable installs can write their .dist-info into each package
|
||||
// (matches the side effect of the non-Docker uvx --with-editable path).
|
||||
if (localSdkPath) {
|
||||
dockerArgs.push("-v", `${localSdkPath}:${CONTAINER_LOCAL_SDK_DIR}`);
|
||||
}
|
||||
|
||||
// Mount credentials / state individually by default so the container
|
||||
// stays isolated from the host home. Opt in to bind-mounting the
|
||||
// entire host home with OH_MOUNT_HOST_HOME=1 — useful when you want
|
||||
// the Add Workspace file browser to navigate your real host
|
||||
// filesystem (those credential subpaths come along automatically as
|
||||
// part of the same mount).
|
||||
if (process.env.OH_MOUNT_HOST_HOME === "1") {
|
||||
dockerArgs.push("-v", `${home}:/home/openhands`);
|
||||
} else {
|
||||
const optionalMounts = [
|
||||
[join(home, ".openhands"), "/home/openhands/.openhands"],
|
||||
[join(home, ".claude"), "/home/openhands/.claude"],
|
||||
[join(home, ".codex"), "/home/openhands/.codex"],
|
||||
[join(home, ".ssh"), "/home/openhands/.ssh"],
|
||||
];
|
||||
for (const [src, dest] of optionalMounts) {
|
||||
if (existsSync(src)) {
|
||||
dockerArgs.push("-v", `${src}:${dest}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +228,7 @@ function startAgentServerDocker(config) {
|
||||
OH_CONVERSATIONS_PATH:
|
||||
"/home/openhands/.openhands/agent-canvas/conversations",
|
||||
OH_PERSISTENCE_DIR: "/home/openhands/.openhands",
|
||||
OH_BASH_EVENTS_DIR:
|
||||
"/home/openhands/.openhands/agent-canvas/bash_events",
|
||||
OH_BASH_EVENTS_DIR: "/home/openhands/.openhands/agent-canvas/bash_events",
|
||||
TMUX_TMPDIR: "/home/openhands/.openhands/agent-canvas/tmux",
|
||||
OH_SECRET_KEY: process.env.OH_SECRET_KEY || DEFAULT_SECRET_KEY,
|
||||
// Required so the secret-seeding PUT /api/settings/secrets call from
|
||||
@@ -191,8 +239,32 @@ function startAgentServerDocker(config) {
|
||||
dockerArgs.push("-e", `${k}=${v}`);
|
||||
}
|
||||
|
||||
// When using a local SDK checkout, override the image's entrypoint to
|
||||
// reinstall the four workspace packages as editable on top of the baked-in
|
||||
// venv, then exec the server. Reusing the image's venv avoids
|
||||
// redownloading transitive deps; editable installs make host-side edits
|
||||
// visible on the next module load (or container restart).
|
||||
if (localSdkPath) {
|
||||
dockerArgs.push("--entrypoint", "/bin/sh");
|
||||
}
|
||||
|
||||
dockerArgs.push(image);
|
||||
|
||||
if (localSdkPath) {
|
||||
const installCmd = [
|
||||
"uv pip install",
|
||||
"--python /agent-server/.venv/bin/python",
|
||||
"--reinstall",
|
||||
`-e ${CONTAINER_LOCAL_SDK_DIR}/openhands-sdk`,
|
||||
`-e ${CONTAINER_LOCAL_SDK_DIR}/openhands-tools`,
|
||||
`-e ${CONTAINER_LOCAL_SDK_DIR}/openhands-workspace`,
|
||||
`-e ${CONTAINER_LOCAL_SDK_DIR}/openhands-agent-server`,
|
||||
].join(" ");
|
||||
const runCmd =
|
||||
"exec /agent-server/.venv/bin/python -m openhands.agent_server --host 0.0.0.0 --port 8000";
|
||||
dockerArgs.push("-c", `${installCmd} && ${runCmd}`);
|
||||
}
|
||||
|
||||
spawnService("agent-server", "docker", dockerArgs, {
|
||||
color: c.blue,
|
||||
});
|
||||
@@ -218,6 +290,7 @@ if (isMainModule) {
|
||||
|
||||
export {
|
||||
AGENT_SERVER_REPO,
|
||||
CONTAINER_LOCAL_SDK_DIR,
|
||||
CONTAINER_NAME,
|
||||
CONTAINER_WORKSPACES_DIR,
|
||||
DEFAULT_AGENT_SERVER_TAG,
|
||||
|
||||
@@ -10,8 +10,15 @@ export interface SubdirectoryPage {
|
||||
next_page_id: string | null;
|
||||
}
|
||||
|
||||
export interface FileBrowserEntry {
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface HomeResponse {
|
||||
home: string;
|
||||
favorites: FileBrowserEntry[];
|
||||
locations: FileBrowserEntry[];
|
||||
}
|
||||
|
||||
export interface SearchSubdirsOptions {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useHomeDirectory,
|
||||
useSearchSubdirs,
|
||||
} from "#/hooks/query/use-search-subdirs";
|
||||
import { useActiveBackend } from "#/contexts/active-backend-context";
|
||||
import { cn } from "#/utils/utils";
|
||||
import FolderIcon from "#/icons/folder.svg?react";
|
||||
import ChevronLeft from "#/icons/chevron-left-small.svg?react";
|
||||
@@ -79,25 +80,6 @@ function getParentPath(path: string): string | null {
|
||||
return trimmed.slice(0, idx);
|
||||
}
|
||||
|
||||
function buildSidebar(home: string | null): {
|
||||
favorites: SidebarEntry[];
|
||||
locations: SidebarEntry[];
|
||||
} {
|
||||
if (!home) {
|
||||
return { favorites: [], locations: [{ label: "/", path: "/" }] };
|
||||
}
|
||||
const trimmed = home.replace(/\/+$/, "");
|
||||
return {
|
||||
favorites: [
|
||||
{ label: "Home", path: trimmed },
|
||||
{ label: "Desktop", path: `${trimmed}/Desktop` },
|
||||
{ label: "Documents", path: `${trimmed}/Documents` },
|
||||
{ label: "Downloads", path: `${trimmed}/Downloads` },
|
||||
],
|
||||
locations: [{ label: "/", path: "/" }],
|
||||
};
|
||||
}
|
||||
|
||||
export function FolderBrowserModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -106,6 +88,7 @@ export function FolderBrowserModal({
|
||||
}: FolderBrowserModalProps) {
|
||||
const { t } = useTranslation("openhands");
|
||||
const [currentPath, setCurrentPath] = useState<string | null>(null);
|
||||
const active = useActiveBackend();
|
||||
|
||||
const { data: homeData } = useHomeDirectory();
|
||||
|
||||
@@ -119,6 +102,12 @@ export function FolderBrowserModal({
|
||||
}
|
||||
}, [isOpen, homeData?.home, currentPath]);
|
||||
|
||||
// A backend switch invalidates the previous path — clear it so the
|
||||
// open/close effect can re-seed from the new backend's homeData.home.
|
||||
useEffect(() => {
|
||||
setCurrentPath(null);
|
||||
}, [active.backend.id, active.orgId]);
|
||||
|
||||
const {
|
||||
data: listing,
|
||||
isLoading,
|
||||
@@ -126,16 +115,34 @@ export function FolderBrowserModal({
|
||||
error,
|
||||
} = useSearchSubdirs(isOpen ? currentPath : null);
|
||||
|
||||
const sidebar = useMemo(
|
||||
() => buildSidebar(homeData?.home ?? null),
|
||||
[homeData?.home],
|
||||
);
|
||||
const favorites: SidebarEntry[] = useMemo(() => {
|
||||
if (!homeData?.home) return [];
|
||||
const trimmed = homeData.home.replace(/[\\/]+$/, "") || homeData.home;
|
||||
return [{ label: "Home", path: trimmed }, ...(homeData.favorites ?? [])];
|
||||
}, [homeData]);
|
||||
|
||||
const locations: SidebarEntry[] = homeData?.locations ?? [];
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const subdirs = listing?.items ?? [];
|
||||
const parent = currentPath ? getParentPath(currentPath) : null;
|
||||
|
||||
// Signal that we're inside the dev:docker container without the host
|
||||
// home mounted: the agent server reports `/home/openhands` as home and
|
||||
// returns no favorites (the only contents are hidden credential dirs).
|
||||
// In that case there's nothing useful for the user to browse, so we
|
||||
// surface the OH_MOUNT_HOST_HOME=1 opt-in instead of the generic empty
|
||||
// state. Off in production / non-Docker dev because favorites are
|
||||
// populated there.
|
||||
const showHostHomeHint =
|
||||
homeData?.home === "/home/openhands" &&
|
||||
(homeData?.favorites?.length ?? 0) === 0 &&
|
||||
currentPath === homeData?.home &&
|
||||
!isLoading &&
|
||||
!isError &&
|
||||
subdirs.length === 0;
|
||||
|
||||
const getBasename = (path: string): string => {
|
||||
const trimmed = path.replace(/\/+$/, "");
|
||||
if (!trimmed) return "/";
|
||||
@@ -194,13 +201,13 @@ export function FolderBrowserModal({
|
||||
>
|
||||
<SidebarSection
|
||||
label={t(I18nKey.HOME$FAVORITES)}
|
||||
entries={sidebar.favorites}
|
||||
entries={favorites}
|
||||
currentPath={currentPath}
|
||||
onPick={setCurrentPath}
|
||||
/>
|
||||
<SidebarSection
|
||||
label={t(I18nKey.HOME$LOCATIONS)}
|
||||
entries={sidebar.locations}
|
||||
entries={locations}
|
||||
currentPath={currentPath}
|
||||
onPick={setCurrentPath}
|
||||
/>
|
||||
@@ -253,8 +260,17 @@ export function FolderBrowserModal({
|
||||
</li>
|
||||
)}
|
||||
{!isLoading && !isError && subdirs.length === 0 && (
|
||||
<li className="px-4 py-2 text-sm text-[#B7BDC2]">
|
||||
{t(I18nKey.HOME$NO_WORKSPACES)}
|
||||
<li
|
||||
className="px-4 py-2 text-sm text-[#B7BDC2]"
|
||||
data-testid={
|
||||
showHostHomeHint
|
||||
? "folder-browser-host-home-hint"
|
||||
: "folder-browser-empty"
|
||||
}
|
||||
>
|
||||
{showHostHomeHint
|
||||
? t(I18nKey.HOME$HOST_HOME_NOT_MOUNTED_HINT)
|
||||
: t(I18nKey.HOME$NO_WORKSPACES)}
|
||||
</li>
|
||||
)}
|
||||
{subdirs.map((entry) => (
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import FilesService from "#/api/files-service/files-service.api";
|
||||
import { useActiveBackend } from "#/contexts/active-backend-context";
|
||||
|
||||
export const useSearchSubdirs = (path: string | null) =>
|
||||
useQuery({
|
||||
queryKey: ["file", "search_subdirs", path],
|
||||
export const useSearchSubdirs = (path: string | null) => {
|
||||
const active = useActiveBackend();
|
||||
return useQuery({
|
||||
queryKey: ["file", "search_subdirs", path, active.backend.id, active.orgId],
|
||||
queryFn: () => FilesService.searchSubdirs(path as string),
|
||||
enabled: !!path,
|
||||
retry: false,
|
||||
meta: { disableToast: true },
|
||||
});
|
||||
};
|
||||
|
||||
export const useHomeDirectory = () =>
|
||||
useQuery({
|
||||
queryKey: ["file", "home"],
|
||||
export const useHomeDirectory = () => {
|
||||
const active = useActiveBackend();
|
||||
return useQuery({
|
||||
queryKey: ["file", "home", active.backend.id, active.orgId],
|
||||
queryFn: () => FilesService.getHome(),
|
||||
retry: false,
|
||||
meta: { disableToast: true },
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -16506,6 +16506,23 @@
|
||||
"uk": "Робочих областей ще немає",
|
||||
"ca": "Encara no hi ha espais de treball"
|
||||
},
|
||||
"HOME$HOST_HOME_NOT_MOUNTED_HINT": {
|
||||
"en": "Your host filesystem isn't visible to the agent server. Set OH_MOUNT_HOST_HOME=1 before npm run dev:docker to browse it from here.",
|
||||
"ja": "ホストのファイルシステムはエージェントサーバーから見えません。ここから参照するには、npm run dev:docker の前に OH_MOUNT_HOST_HOME=1 を設定してください。",
|
||||
"zh-CN": "代理服务器看不到您的宿主机文件系统。请在运行 npm run dev:docker 之前设置 OH_MOUNT_HOST_HOME=1,即可在此浏览。",
|
||||
"zh-TW": "代理伺服器看不到您的主機檔案系統。請在執行 npm run dev:docker 之前設定 OH_MOUNT_HOST_HOME=1,即可在此瀏覽。",
|
||||
"ko-KR": "에이전트 서버에서 호스트 파일 시스템이 보이지 않습니다. 여기서 탐색하려면 npm run dev:docker 전에 OH_MOUNT_HOST_HOME=1을 설정하세요.",
|
||||
"no": "Vertens filsystem er ikke synlig for agent-serveren. Sett OH_MOUNT_HOST_HOME=1 før npm run dev:docker for å bla gjennom det herfra.",
|
||||
"it": "Il filesystem dell'host non è visibile al server dell'agente. Imposta OH_MOUNT_HOST_HOME=1 prima di npm run dev:docker per esplorarlo da qui.",
|
||||
"pt": "O sistema de arquivos do host não está visível para o servidor do agente. Defina OH_MOUNT_HOST_HOME=1 antes de npm run dev:docker para navegá-lo daqui.",
|
||||
"es": "El sistema de archivos del anfitrión no es visible para el servidor del agente. Establece OH_MOUNT_HOST_HOME=1 antes de npm run dev:docker para explorarlo desde aquí.",
|
||||
"ar": "نظام ملفات المضيف غير مرئي لخادم الوكيل. اضبط OH_MOUNT_HOST_HOME=1 قبل npm run dev:docker لتصفحه من هنا.",
|
||||
"fr": "Le système de fichiers de l'hôte n'est pas visible par le serveur de l'agent. Définissez OH_MOUNT_HOST_HOME=1 avant npm run dev:docker pour le parcourir d'ici.",
|
||||
"tr": "Ana makine dosya sistemi ajan sunucusu tarafından görülmüyor. Buradan göz atmak için npm run dev:docker öncesinde OH_MOUNT_HOST_HOME=1 ayarlayın.",
|
||||
"de": "Das Host-Dateisystem ist für den Agenten-Server nicht sichtbar. Setze OH_MOUNT_HOST_HOME=1 vor npm run dev:docker, um es hier zu durchsuchen.",
|
||||
"uk": "Файлова система хоста не видима для сервера агента. Встановіть OH_MOUNT_HOST_HOME=1 перед npm run dev:docker, щоб переглядати її звідси.",
|
||||
"ca": "El sistema de fitxers de l'amfitrió no és visible per al servidor de l'agent. Establiu OH_MOUNT_HOST_HOME=1 abans de npm run dev:docker per navegar-hi des d'aquí."
|
||||
},
|
||||
"HOME$NO_WORKSPACE_OPTION": {
|
||||
"en": "No workspace",
|
||||
"ja": "ワークスペースなし",
|
||||
|
||||
Reference in New Issue
Block a user