Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions opencode-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2305,9 +2305,10 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) {
// -----------------------------------------------------------------------

function getSkillsCli() {
const command = process.platform === "win32" ? "npx.cmd" : "npx";
try {
execSync("npx skills --version", { stdio: "ignore", timeout: 10_000 });
return "npx";
execSync(`${command} skills --version`, { stdio: "ignore", timeout: 10_000 });
return command;
} catch {
return null;
}
Expand All @@ -2323,10 +2324,12 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) {
const args = ["skills", "add", source, "-y"];
if (globalScope) args.push("-g");
const env = { ...process.env, DISABLE_TELEMETRY: "1", DO_NOT_TRACK: "1" };
const workingDirectory = globalScope || !cwd || cwd === "/" ? homedir() : cwd;
const child = spawn(cli, args, {
cwd: globalScope ? homedir() : cwd,
cwd: workingDirectory,
stdio: ["ignore", "pipe", "pipe"],
env,
shell: process.platform === "win32",
windowsHide: true,
});

Expand Down Expand Up @@ -2375,10 +2378,13 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) {
const args = ["skills", "rm", skillName, "-y"];
if (globalScope) args.push("-g");
const env = { ...process.env, DISABLE_TELEMETRY: "1", DO_NOT_TRACK: "1" };
const workingDirectory =
globalScope || !directory || directory === "/" ? homedir() : directory;
const child = spawn(cli, args, {
cwd: globalScope ? homedir() : directory,
cwd: workingDirectory,
stdio: ["ignore", "pipe", "pipe"],
env,
shell: process.platform === "win32",
windowsHide: true,
});

Expand Down Expand Up @@ -2417,10 +2423,13 @@ export function setupOpenCodeBridge(ipcMain, _getWindows) {
if (globalScope) args.push("-g");
args.push("-y");
const env = { ...process.env, DISABLE_TELEMETRY: "1", DO_NOT_TRACK: "1" };
const workingDirectory =
globalScope || !directory || directory === "/" ? homedir() : directory;
const child = spawn(cli, args, {
cwd: globalScope ? homedir() : directory,
cwd: workingDirectory,
stdio: ["ignore", "pipe", "pipe"],
env,
shell: process.platform === "win32",
windowsHide: true,
});

Expand Down
14 changes: 10 additions & 4 deletions server/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { basename, dirname, extname, join, resolve } from "node:path";
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
import type { HarnessEvent } from "../src/agents/backend.ts";
import type { HarnessId } from "../src/agents/index.ts";
import {
Expand Down Expand Up @@ -425,6 +425,13 @@ function parseAllowedRoots() {

const allowedRoots = parseAllowedRoots();

function isWithinAllowedRoot(path: string) {
return allowedRoots.some((root) => {
const relativePath = relative(root, path);
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
});
}

function parsePositiveIntegerEnv(name: string, fallback: number) {
const raw = process.env[name]?.trim();
if (!raw) return fallback;
Expand Down Expand Up @@ -488,8 +495,7 @@ async function resolveSafeDirectory(inputPath: string | null) {
const actual = await realpath(requested);
const info = await stat(actual);
if (!info.isDirectory()) throw new Error("Path is not a directory");
const allowed = allowedRoots.some((root) => actual === root || actual.startsWith(`${root}/`));
if (!allowed) throw new Error("Path outside OPENGUI_ALLOWED_ROOTS");
if (!isWithinAllowedRoot(actual)) throw new Error("Path outside OPENGUI_ALLOWED_ROOTS");
return actual;
}

Expand All @@ -501,7 +507,7 @@ async function listServerDirectories(inputPath: string | null) {
.map((entry) => ({ name: entry.name, path: join(path, entry.name), type: "dir" as const }))
.sort((a, b) => a.name.localeCompare(b.name));
const parent = dirname(path);
const canGoUp = allowedRoots.some((root) => parent === root || parent.startsWith(`${root}/`));
const canGoUp = isWithinAllowedRoot(parent);
return { path, parent: canGoUp ? parent : null, roots: allowedRoots, entries: dirs };
}

Expand Down
13 changes: 10 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@ import { SetupWizard } from "./components/SetupWizard";
import { TitleBar } from "./components/TitleBar";
import "./index.css";

function AppContent({ detachedProject }: { detachedProject?: string }) {
function AppContent({
detachedProject,
suppressBootErrors,
}: {
detachedProject?: string;
suppressBootErrors?: boolean;
}) {
const client = useOpenGuiClient();
const lastEscapeAtRef = useRef(0);
const [queueMode, setQueueMode] = useState<QueueMode>("queue");
Expand Down Expand Up @@ -288,14 +294,15 @@ function AppContent({ detachedProject }: { detachedProject?: string }) {
const isBooting = bootState === "checking-server" || bootState === "starting-server";

useEffect(() => {
if (suppressBootErrors) return;
if (isBooting) return;
const message = bootState === "error" ? bootError : lastError;
if (!message) return;
toast.error(message, {
description: bootState === "error" && normalizedBootLogs ? normalizedBootLogs : undefined,
duration: 8000,
});
}, [bootState, bootError, isBooting, lastError, normalizedBootLogs]);
}, [bootState, bootError, isBooting, lastError, normalizedBootLogs, suppressBootErrors]);

useEffect(() => {
let cancelled = false;
Expand Down Expand Up @@ -529,7 +536,7 @@ export function App() {
<OpenGuiClientProvider>
<HarnessProvider detachedProject={detachedProject}>
<SidebarProvider className="!h-dvh capacitor-safe-area">
<AppContent detachedProject={detachedProject} />
<AppContent detachedProject={detachedProject} suppressBootErrors={showWizard} />
{showWizard && <SetupWizard onComplete={() => setShowWizard(false)} />}
<Toaster richColors closeButton />
</SidebarProvider>
Expand Down
43 changes: 34 additions & 9 deletions src/components/DiscoverPlugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { toast } from "sonner";
import { Globe, Loader2, Search, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { usePluginsPlatform } from "@/hooks/use-plugins-platform";
import { useConnectionState } from "@/hooks/use-agent-state";
import { useActions, useConnectionState } from "@/hooks/use-agent-state";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import {
Expand Down Expand Up @@ -69,6 +69,7 @@ export function DiscoverPlugins() {
const pluginsApi = usePluginsPlatform();
const catalogApi = pluginsApi?.marketplace;
const { activeDirectory } = useConnectionState();
const { refreshProviders } = useActions();
const shell = useDesktopShell();

const [query, setQuery] = useState("");
Expand Down Expand Up @@ -198,7 +199,11 @@ export function DiscoverPlugins() {
);

const runPluginAction = useCallback(
async (key: string, action: () => Promise<void>, skillName?: string) => {
async (
key: string,
action: () => Promise<{ exitCode?: number } | void>,
skillName?: string,
) => {
setBusyKeys((prev) => new Set(prev).add(key));
setInstallProgress({
phase: "starting",
Expand All @@ -207,16 +212,28 @@ export function DiscoverPlugins() {
});
setShowProgress(true);
try {
await action();
await fetchInstalled();
} catch {}
const result = await action();
if (result?.exitCode !== undefined && result.exitCode !== 0) {
setInstallProgress((prev) => ({ ...prev, phase: "failed" }));
} else {
setInstallProgress((prev) => ({ ...prev, phase: "completed" }));
await fetchInstalled();
await refreshProviders();
}
} catch (error) {
setInstallProgress((prev) => ({
...prev,
phase: "failed",
rawLines: [...prev.rawLines, error instanceof Error ? error.message : String(error)],
}));
}
setBusyKeys((prev) => {
const next = new Set(prev);
next.delete(key);
return next;
});
},
[fetchInstalled],
[fetchInstalled, refreshProviders],
);

// Install
Expand All @@ -230,7 +247,11 @@ export function DiscoverPlugins() {
if (!pluginsApi) return;
const key = installed.remoteKey || installed.location;
await runPluginAction(key, async () => {
await pluginsApi.update(installed.name, scopedDirectory, installed.scope === "global");
return await pluginsApi.update(
installed.name,
scopedDirectory,
installed.scope === "global",
);
});
},
[pluginsApi, scopedDirectory, runPluginAction],
Expand All @@ -241,7 +262,11 @@ export function DiscoverPlugins() {
if (!pluginsApi) return;
const key = installed.remoteKey || installed.location;
await runPluginAction(key, async () => {
await pluginsApi.remove(installed.name, scopedDirectory, installed.scope === "global");
return await pluginsApi.remove(
installed.name,
scopedDirectory,
installed.scope === "global",
);
});
},
[pluginsApi, scopedDirectory, runPluginAction],
Expand All @@ -254,7 +279,7 @@ export function DiscoverPlugins() {
await runPluginAction(
key,
async () => {
await pluginsApi.install(source, scopedDirectory, globalScope);
return await pluginsApi.install(source, scopedDirectory, globalScope);
},
installPlugin.name,
);
Expand Down
25 changes: 18 additions & 7 deletions src/components/SetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
LoaderCircle,
RotateCw,
Terminal,
X,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
Expand Down Expand Up @@ -136,8 +137,18 @@ export function SetupWizard({ onComplete }: Props) {

return (
<div className="fixed inset-0 z-50 overflow-y-auto bg-background/90 backdrop-blur-md">
<div className="flex min-h-full items-start justify-center px-5 py-6 sm:items-center sm:py-10">
<div className="w-full max-w-[680px]">
<div className="flex min-h-full items-start justify-center px-4 py-4 sm:items-center sm:py-6">
<div className="relative max-h-[calc(100dvh-2rem)] w-full max-w-[560px] overflow-y-auto rounded-2xl border bg-background p-4 shadow-xl sm:max-h-[calc(100dvh-3rem)] sm:p-5">
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-2 size-8"
aria-label={t("common.close")}
onClick={onComplete}
>
<X className="size-4" />
</Button>
<div className="mb-5 text-center">
<div className="mb-2 text-xs text-muted-foreground">
{Math.max(currentStepNumber, 0) + 1} / 4
Expand All @@ -163,7 +174,7 @@ export function SetupWizard({ onComplete }: Props) {
</div>

{step === "harness" && (
<div className="rounded-xl border bg-card p-5 shadow-sm">
<div className="rounded-xl border bg-card p-4 shadow-sm sm:p-5">
{harnessState === "detecting" && (
<StatusRow
icon={<LoaderCircle className="size-5 animate-spin" />}
Expand Down Expand Up @@ -217,7 +228,7 @@ export function SetupWizard({ onComplete }: Props) {
)}

{step === "opencode" && (
<div className="rounded-xl border bg-card p-5 shadow-sm">
<div className="rounded-xl border bg-card p-4 shadow-sm sm:p-5">
<StatusRow
icon={
opencodeInstalled ? (
Expand Down Expand Up @@ -287,7 +298,7 @@ export function SetupWizard({ onComplete }: Props) {
)}

{step === "folder" && (
<div className="rounded-xl border bg-card p-5 shadow-sm">
<div className="rounded-xl border bg-card p-4 shadow-sm sm:p-5">
<StatusRow
icon={<Folder className="size-5 text-muted-foreground" />}
title={t("setupWizard.defaultChatDirectoryTitle")}
Expand All @@ -310,14 +321,14 @@ export function SetupWizard({ onComplete }: Props) {
)}

{step === "appearance" && (
<div className="rounded-xl border bg-card p-5 shadow-sm">
<div className="rounded-xl border bg-card p-4 shadow-sm sm:p-5">
<AppearanceSetting />
<StepNav onBack={() => setStep("folder")} onNext={() => setStep("finish")} />
</div>
)}

{step === "finish" && (
<div className="rounded-xl border bg-card p-5 shadow-sm">
<div className="rounded-xl border bg-card p-4 shadow-sm sm:p-5">
<StatusRow
icon={<Check className="size-5 text-emerald-500" />}
title={
Expand Down
Loading