mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
Refactor onboarding for startup preferences (#400)
* refactor(app): align onboarding with startup preferences * chore: refresh pnpm lockfile * feat(web): proxy OpenCode through OpenWork
This commit is contained in:
@@ -84,6 +84,7 @@ setItems((current) => current.filter((x) => x.id !== id));
|
||||
- Could two async actions overlap and fight over one boolean?
|
||||
- Is any UI state duplicated (can be derived instead)?
|
||||
- Do event handlers read signals after an `await` where values might have changed?
|
||||
- If you refactor props/types, did you update all intermediate component signatures and call sites?
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ Read INFRASTRUCTURE.md
|
||||
## Repository Guidance
|
||||
|
||||
* Write new PRDs under `packages/app/pr/<prd-name>.md` (see `.opencode/skills/prd-conventions/SKILL.md`).
|
||||
* If other workspace instructions mention a different PRD location, default to this repo's guidance for OpenWork work.
|
||||
* Use `VISION.md`, `PRINCIPLES.md`, `PRODUCT.md`, `ARCHITECTURE.md`, and `INFRASTRUCTURE.md` to understand the "why" and requirements so you can guide your decisions.
|
||||
|
||||
## Local Structure
|
||||
|
||||
@@ -55,7 +55,7 @@ import type {
|
||||
Client,
|
||||
DashboardTab,
|
||||
MessageWithParts,
|
||||
Mode,
|
||||
StartupPreference,
|
||||
EngineRuntime,
|
||||
ModelOption,
|
||||
ModelRef,
|
||||
@@ -83,7 +83,7 @@ import type {
|
||||
ScheduledJob,
|
||||
} from "./types";
|
||||
import {
|
||||
clearModePreference,
|
||||
clearStartupPreference,
|
||||
deriveArtifacts,
|
||||
deriveWorkingFiles,
|
||||
formatBytes,
|
||||
@@ -102,10 +102,9 @@ import {
|
||||
lastUserModelFromMessages,
|
||||
// normalizeDirectoryPath,
|
||||
parseModelRef,
|
||||
readModePreference,
|
||||
readStartupPreference,
|
||||
safeStringify,
|
||||
summarizeStep,
|
||||
writeModePreference,
|
||||
addOpencodeCacheHint,
|
||||
} from "./utils";
|
||||
import {
|
||||
@@ -222,10 +221,10 @@ export default function App() {
|
||||
navigate(`/session/${trimmed}`, options);
|
||||
};
|
||||
|
||||
const [mode, setMode] = createSignal<Mode | null>(null);
|
||||
const [startupPreference, setStartupPreference] = createSignal<StartupPreference | null>(null);
|
||||
const [onboardingStep, setOnboardingStep] =
|
||||
createSignal<OnboardingStep>("mode");
|
||||
const [rememberModeChoice, setRememberModeChoice] = createSignal(false);
|
||||
createSignal<OnboardingStep>("welcome");
|
||||
const [rememberStartupChoice, setRememberStartupChoice] = createSignal(false);
|
||||
const [themeMode, setThemeMode] = createSignal<ThemeMode>(getInitialThemeMode());
|
||||
|
||||
const [engineSource, setEngineSource] = createSignal<"path" | "sidecar">(
|
||||
@@ -251,35 +250,44 @@ export default function App() {
|
||||
const [openworkAuditStatus, setOpenworkAuditStatus] = createSignal<"idle" | "loading" | "error">("idle");
|
||||
const [openworkAuditError, setOpenworkAuditError] = createSignal<string | null>(null);
|
||||
const [devtoolsWorkspaceId, setDevtoolsWorkspaceId] = createSignal<string | null>(null);
|
||||
const [hostOpenworkCapabilities, setHostOpenworkCapabilities] = createSignal<OpenworkServerCapabilities | null>(null);
|
||||
|
||||
const openworkServerBaseUrl = createMemo(() => {
|
||||
const pref = startupPreference();
|
||||
const hostInfo = openworkServerHostInfo();
|
||||
const settingsUrl = normalizeOpenworkServerUrl(openworkServerSettings().urlOverride ?? "") ?? "";
|
||||
|
||||
if (pref === "local") return hostInfo?.baseUrl ?? "";
|
||||
if (pref === "server") return settingsUrl;
|
||||
return hostInfo?.baseUrl ?? settingsUrl;
|
||||
});
|
||||
|
||||
const openworkServerAuth = createMemo(() => {
|
||||
const pref = startupPreference();
|
||||
const hostInfo = openworkServerHostInfo();
|
||||
const settingsToken = openworkServerSettings().token?.trim() ?? "";
|
||||
const clientToken = hostInfo?.clientToken?.trim() ?? "";
|
||||
const hostToken = hostInfo?.hostToken?.trim() ?? "";
|
||||
|
||||
if (pref === "local") {
|
||||
return { token: clientToken || undefined, hostToken: hostToken || undefined };
|
||||
}
|
||||
if (pref === "server") {
|
||||
return { token: settingsToken || undefined, hostToken: undefined };
|
||||
}
|
||||
if (hostInfo?.baseUrl) {
|
||||
return { token: clientToken || undefined, hostToken: hostToken || undefined };
|
||||
}
|
||||
return { token: settingsToken || undefined, hostToken: undefined };
|
||||
});
|
||||
|
||||
const openworkServerClient = createMemo(() => {
|
||||
const currentMode = mode();
|
||||
if (currentMode === "client") {
|
||||
const url = openworkServerUrl().trim();
|
||||
if (!url) return null;
|
||||
const token = openworkServerSettings().token;
|
||||
return createOpenworkServerClient({ baseUrl: url, token });
|
||||
}
|
||||
if (currentMode === "host") {
|
||||
const info = openworkServerHostInfo();
|
||||
if (!info?.baseUrl) return null;
|
||||
return createOpenworkServerClient({
|
||||
baseUrl: info.baseUrl,
|
||||
token: info.clientToken ?? undefined,
|
||||
hostToken: info.hostToken ?? undefined,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
const baseUrl = openworkServerBaseUrl().trim();
|
||||
if (!baseUrl) return null;
|
||||
const auth = openworkServerAuth();
|
||||
return createOpenworkServerClient({ baseUrl, token: auth.token, hostToken: auth.hostToken });
|
||||
});
|
||||
|
||||
const devtoolsOpenworkClient = createMemo(() => {
|
||||
if (mode() === "client") return openworkServerClient();
|
||||
if (mode() !== "host") return null;
|
||||
const info = openworkServerHostInfo();
|
||||
if (!info?.baseUrl || !info?.clientToken) return null;
|
||||
return createOpenworkServerClient({ baseUrl: info.baseUrl, token: info.clientToken });
|
||||
});
|
||||
const devtoolsOpenworkClient = createMemo(() => openworkServerClient());
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
@@ -287,22 +295,24 @@ export default function App() {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (mode() === "client") {
|
||||
const override = normalizeOpenworkServerUrl(openworkServerSettings().urlOverride ?? "");
|
||||
setOpenworkServerUrl(override ?? "");
|
||||
const pref = startupPreference();
|
||||
const info = openworkServerHostInfo();
|
||||
const hostUrl = info?.connectUrl ?? info?.lanUrl ?? info?.mdnsUrl ?? info?.baseUrl ?? "";
|
||||
const settingsUrl = normalizeOpenworkServerUrl(openworkServerSettings().urlOverride ?? "") ?? "";
|
||||
|
||||
if (pref === "local") {
|
||||
setOpenworkServerUrl(hostUrl);
|
||||
return;
|
||||
}
|
||||
if (mode() === "host") {
|
||||
const info = openworkServerHostInfo();
|
||||
const resolved = info?.connectUrl ?? info?.lanUrl ?? info?.mdnsUrl ?? info?.baseUrl ?? "";
|
||||
setOpenworkServerUrl(resolved ?? "");
|
||||
if (pref === "server") {
|
||||
setOpenworkServerUrl(settingsUrl);
|
||||
return;
|
||||
}
|
||||
setOpenworkServerUrl("");
|
||||
setOpenworkServerUrl(hostUrl || settingsUrl);
|
||||
});
|
||||
|
||||
const checkOpenworkServer = async (url: string, token?: string) => {
|
||||
const client = createOpenworkServerClient({ baseUrl: url, token });
|
||||
const checkOpenworkServer = async (url: string, token?: string, hostToken?: string) => {
|
||||
const client = createOpenworkServerClient({ baseUrl: url, token, hostToken });
|
||||
try {
|
||||
await client.health();
|
||||
} catch (error) {
|
||||
@@ -329,9 +339,10 @@ export default function App() {
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (mode() !== "client") return;
|
||||
const url = openworkServerUrl().trim();
|
||||
const token = openworkServerSettings().token;
|
||||
const url = openworkServerBaseUrl().trim();
|
||||
const auth = openworkServerAuth();
|
||||
const token = auth.token;
|
||||
const hostToken = auth.hostToken;
|
||||
|
||||
if (!url) {
|
||||
setOpenworkServerStatus("disconnected");
|
||||
@@ -347,7 +358,7 @@ export default function App() {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const result = await checkOpenworkServer(url, token);
|
||||
const result = await checkOpenworkServer(url, token, hostToken);
|
||||
if (!active) return;
|
||||
setOpenworkServerStatus(result.status);
|
||||
setOpenworkServerCapabilities(result.capabilities);
|
||||
@@ -366,59 +377,8 @@ export default function App() {
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (mode() !== "host") return;
|
||||
const info = openworkServerHostInfo();
|
||||
const running = info?.running ?? false;
|
||||
setOpenworkServerStatus(running ? "connected" : "disconnected");
|
||||
setOpenworkServerCapabilities(null);
|
||||
setOpenworkServerCheckedAt(Date.now());
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (mode() !== "host") {
|
||||
setHostOpenworkCapabilities(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const client = openworkServerClient();
|
||||
if (!client || openworkServerStatus() !== "connected") {
|
||||
setHostOpenworkCapabilities(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
let busy = false;
|
||||
|
||||
const run = async () => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const caps = await client.capabilities();
|
||||
if (active) setHostOpenworkCapabilities(caps);
|
||||
} catch {
|
||||
if (active) setHostOpenworkCapabilities(null);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
const interval = window.setInterval(run, 10_000);
|
||||
onCleanup(() => {
|
||||
active = false;
|
||||
window.clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
if (mode() !== "host") {
|
||||
setOpenworkServerHostInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
|
||||
const run = async () => {
|
||||
@@ -1080,7 +1040,6 @@ export default function App() {
|
||||
|
||||
const extensionsStore = createExtensionsStore({
|
||||
client,
|
||||
mode,
|
||||
projectDir: () => workspaceProjectDir(),
|
||||
activeWorkspaceRoot: () => workspaceStore.activeWorkspaceRoot(),
|
||||
workspaceType: () => workspaceStore.activeWorkspaceDisplay().workspaceType,
|
||||
@@ -1282,11 +1241,12 @@ export default function App() {
|
||||
let loadCommandsRef: (options?: { workspaceRoot?: string; quiet?: boolean }) => Promise<void> = async () => {};
|
||||
|
||||
const workspaceStore = createWorkspaceStore({
|
||||
mode,
|
||||
setMode,
|
||||
startupPreference,
|
||||
setStartupPreference,
|
||||
onboardingStep,
|
||||
setOnboardingStep,
|
||||
rememberModeChoice,
|
||||
rememberStartupChoice,
|
||||
setRememberStartupChoice,
|
||||
baseUrl,
|
||||
setBaseUrl,
|
||||
clientDirectory,
|
||||
@@ -1330,8 +1290,14 @@ export default function App() {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (mode() !== "client") return;
|
||||
const active = workspaceStore.activeWorkspaceDisplay();
|
||||
const client = openworkServerClient();
|
||||
|
||||
if (!client || openworkServerStatus() !== "connected") {
|
||||
setOpenworkServerWorkspaceId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (active.workspaceType === "remote" && active.remoteType === "openwork") {
|
||||
const storedId = active.openworkWorkspaceId ?? null;
|
||||
if (storedId) {
|
||||
@@ -1339,14 +1305,7 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = openworkServerClient();
|
||||
if (!client || openworkServerStatus() !== "connected") {
|
||||
setOpenworkServerWorkspaceId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const resolveWorkspace = async () => {
|
||||
try {
|
||||
const response = await client.listWorkspaces();
|
||||
@@ -1359,58 +1318,51 @@ export default function App() {
|
||||
};
|
||||
|
||||
void resolveWorkspace();
|
||||
|
||||
onCleanup(() => {
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setOpenworkServerWorkspaceId(null);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (mode() !== "host") return;
|
||||
const root = normalizeDirectoryPath(workspaceStore.activeWorkspaceRoot().trim());
|
||||
const client = openworkServerClient();
|
||||
|
||||
if (!root || !client || openworkServerStatus() !== "connected") {
|
||||
setOpenworkServerWorkspaceId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const resolveWorkspace = async () => {
|
||||
try {
|
||||
const response = await client.listWorkspaces();
|
||||
if (cancelled) return;
|
||||
const items = Array.isArray(response.items) ? response.items : [];
|
||||
const match = items.find((entry) => normalizeDirectoryPath(entry.path) === root);
|
||||
setOpenworkServerWorkspaceId(response.activeId ?? match?.id ?? null);
|
||||
} catch {
|
||||
if (!cancelled) setOpenworkServerWorkspaceId(null);
|
||||
if (active.workspaceType === "local") {
|
||||
const root = normalizeDirectoryPath(workspaceStore.activeWorkspaceRoot().trim());
|
||||
if (!root) {
|
||||
setOpenworkServerWorkspaceId(null);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
void resolveWorkspace();
|
||||
let cancelled = false;
|
||||
const resolveWorkspace = async () => {
|
||||
try {
|
||||
const response = await client.listWorkspaces();
|
||||
if (cancelled) return;
|
||||
const items = Array.isArray(response.items) ? response.items : [];
|
||||
const match = items.find((entry) => normalizeDirectoryPath(entry.path) === root);
|
||||
setOpenworkServerWorkspaceId(response.activeId ?? match?.id ?? null);
|
||||
} catch {
|
||||
if (!cancelled) setOpenworkServerWorkspaceId(null);
|
||||
}
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
cancelled = true;
|
||||
});
|
||||
void resolveWorkspace();
|
||||
onCleanup(() => {
|
||||
cancelled = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenworkServerWorkspaceId(null);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!developerMode()) {
|
||||
setDevtoolsWorkspaceId(null);
|
||||
setHostOpenworkCapabilities(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const client = devtoolsOpenworkClient();
|
||||
if (!client) {
|
||||
setDevtoolsWorkspaceId(null);
|
||||
setHostOpenworkCapabilities(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1419,15 +1371,6 @@ export default function App() {
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
if (mode() === "host") {
|
||||
try {
|
||||
const caps = await client.capabilities();
|
||||
if (active) setHostOpenworkCapabilities(caps);
|
||||
} catch {
|
||||
if (active) setHostOpenworkCapabilities(null);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await client.listWorkspaces();
|
||||
if (!active) return;
|
||||
const items = Array.isArray(response.items) ? response.items : [];
|
||||
@@ -1512,9 +1455,7 @@ export default function App() {
|
||||
|
||||
const openworkServerReady = createMemo(() => openworkServerStatus() === "connected");
|
||||
const openworkServerWorkspaceReady = createMemo(() => Boolean(openworkServerWorkspaceId()));
|
||||
const resolvedOpenworkCapabilities = createMemo(() =>
|
||||
mode() === "host" ? hostOpenworkCapabilities() : openworkServerCapabilities(),
|
||||
);
|
||||
const resolvedOpenworkCapabilities = createMemo(() => openworkServerCapabilities());
|
||||
const openworkServerCanWriteSkills = createMemo(
|
||||
() =>
|
||||
openworkServerReady() &&
|
||||
@@ -1527,9 +1468,7 @@ export default function App() {
|
||||
openworkServerWorkspaceReady() &&
|
||||
(resolvedOpenworkCapabilities()?.plugins?.write ?? false),
|
||||
);
|
||||
const devtoolsCapabilities = createMemo(() =>
|
||||
mode() === "host" ? hostOpenworkCapabilities() : openworkServerCapabilities(),
|
||||
);
|
||||
const devtoolsCapabilities = createMemo(() => openworkServerCapabilities());
|
||||
const resolvedDevtoolsWorkspaceId = createMemo(() => devtoolsWorkspaceId() ?? openworkServerWorkspaceId());
|
||||
|
||||
function updateOpenworkServerSettings(next: OpenworkServerSettings) {
|
||||
@@ -1550,11 +1489,24 @@ export default function App() {
|
||||
setOpenworkServerCheckedAt(Date.now());
|
||||
return false;
|
||||
}
|
||||
const result = await checkOpenworkServer(derived, next.token);
|
||||
const result = await checkOpenworkServer(derived, next.token, openworkServerAuth().hostToken);
|
||||
setOpenworkServerStatus(result.status);
|
||||
setOpenworkServerCapabilities(result.capabilities);
|
||||
setOpenworkServerCheckedAt(Date.now());
|
||||
return result.status === "connected" || result.status === "limited";
|
||||
const ok = result.status === "connected" || result.status === "limited";
|
||||
if (ok && !isTauriRuntime()) {
|
||||
const active = workspaceStore.activeWorkspaceDisplay();
|
||||
const shouldAttach = !client() || active.workspaceType !== "remote" || active.remoteType !== "openwork";
|
||||
if (shouldAttach) {
|
||||
await workspaceStore
|
||||
.createRemoteWorkspaceFlow({
|
||||
openworkHostUrl: derived,
|
||||
openworkToken: next.token ?? null,
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
};
|
||||
|
||||
const commandState = createCommandState({
|
||||
@@ -1972,12 +1924,12 @@ export default function App() {
|
||||
};
|
||||
|
||||
const canReloadViaOpenworkServer = createMemo(() => Boolean(resolveOpenworkReloadTarget()));
|
||||
const canReloadLocalEngine = () =>
|
||||
isTauriRuntime() && workspaceStore.activeWorkspaceDisplay().workspaceType === "local";
|
||||
|
||||
const canReloadWorkspace = createMemo(() => {
|
||||
if (canReloadViaOpenworkServer()) return true;
|
||||
if (mode() !== "host") return false;
|
||||
if (!isTauriRuntime()) return false;
|
||||
return true;
|
||||
return canReloadLocalEngine();
|
||||
});
|
||||
|
||||
const reloadWorkspaceEngineFromUi = async () => {
|
||||
@@ -1997,7 +1949,7 @@ export default function App() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (mode() === "host" && isTauriRuntime()) {
|
||||
if (canReloadLocalEngine()) {
|
||||
return workspaceStore.reloadWorkspaceEngine();
|
||||
}
|
||||
throw new Error("OpenWork server reload endpoint not found. Update the host to enable reloads.");
|
||||
@@ -2005,7 +1957,7 @@ export default function App() {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (mode() !== "host" || !isTauriRuntime()) {
|
||||
if (!canReloadLocalEngine()) {
|
||||
throw new Error("Reload is unavailable for this workspace.");
|
||||
}
|
||||
return workspaceStore.reloadWorkspaceEngine();
|
||||
@@ -2013,7 +1965,6 @@ export default function App() {
|
||||
|
||||
const systemState = createSystemState({
|
||||
client,
|
||||
mode,
|
||||
sessions,
|
||||
sessionStatusById,
|
||||
refreshPlugins,
|
||||
@@ -2663,27 +2614,9 @@ export default function App() {
|
||||
}
|
||||
|
||||
|
||||
function clearOpenworkLocalStorage() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
const keys = Object.keys(window.localStorage);
|
||||
for (const key of keys) {
|
||||
if (key.startsWith("openwork.")) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
// Legacy compatibility key
|
||||
window.localStorage.removeItem("openwork_mode_pref");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function connectNotion() {
|
||||
if (mode() !== "host") {
|
||||
setNotionError("Notion connections are only available in Host mode.");
|
||||
if (workspaceStore.activeWorkspaceDisplay().workspaceType !== "local") {
|
||||
setNotionError("Notion connections are only available for local workspaces.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2770,7 +2703,7 @@ export default function App() {
|
||||
async function refreshMcpServers() {
|
||||
const projectDir = workspaceProjectDir().trim();
|
||||
const isRemoteWorkspace = workspaceStore.activeWorkspaceDisplay().workspaceType === "remote";
|
||||
const isHostMode = mode() === "host";
|
||||
const isLocalWorkspace = !isRemoteWorkspace;
|
||||
const openworkClient = openworkServerClient();
|
||||
const openworkWorkspaceId = openworkServerWorkspaceId();
|
||||
const openworkCapabilities = resolvedOpenworkCapabilities();
|
||||
@@ -2821,7 +2754,7 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isHostMode && canUseOpenworkServer) {
|
||||
if (isLocalWorkspace && canUseOpenworkServer) {
|
||||
try {
|
||||
setMcpStatus(null);
|
||||
const response = await openworkClient.listMcp(openworkWorkspaceId);
|
||||
@@ -2856,7 +2789,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
if (!isTauriRuntime()) {
|
||||
setMcpStatus("MCP configuration is only available in Host mode.");
|
||||
setMcpStatus("MCP configuration is only available for local workspaces.");
|
||||
setMcpServers([]);
|
||||
setMcpStatuses({});
|
||||
return;
|
||||
@@ -2906,19 +2839,9 @@ export default function App() {
|
||||
async function connectMcp(entry: (typeof MCP_QUICK_CONNECT)[number]) {
|
||||
console.log("[connectMcp] called with entry:", entry);
|
||||
|
||||
if (mode() !== "host") {
|
||||
console.log("[connectMcp] ❌ mode is not host, mode=", mode());
|
||||
setMcpStatus(t("mcp.host_mode_only", currentLocale()));
|
||||
return;
|
||||
}
|
||||
|
||||
const isRemoteWorkspace = workspaceStore.activeWorkspaceDisplay().workspaceType === "remote";
|
||||
const projectDir = workspaceProjectDir().trim();
|
||||
console.log("[connectMcp] projectDir:", projectDir);
|
||||
if (!projectDir) {
|
||||
console.log("[connectMcp] ❌ no projectDir");
|
||||
setMcpStatus(t("mcp.pick_workspace_first", currentLocale()));
|
||||
return;
|
||||
}
|
||||
|
||||
const openworkClient = openworkServerClient();
|
||||
const openworkWorkspaceId = openworkServerWorkspaceId();
|
||||
@@ -2929,6 +2852,12 @@ export default function App() {
|
||||
openworkWorkspaceId &&
|
||||
openworkCapabilities?.mcp?.write;
|
||||
|
||||
if (isRemoteWorkspace && !canUseOpenworkServer) {
|
||||
console.log("[connectMcp] ❌ openwork server unavailable");
|
||||
setMcpStatus("OpenWork server unavailable. MCP config is read-only.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canUseOpenworkServer && !isTauriRuntime()) {
|
||||
console.log("[connectMcp] ❌ not Tauri runtime");
|
||||
setMcpStatus(t("mcp.desktop_required", currentLocale()));
|
||||
@@ -2936,6 +2865,12 @@ export default function App() {
|
||||
}
|
||||
console.log("[connectMcp] ✓ runtime ready");
|
||||
|
||||
if (!isRemoteWorkspace && !projectDir) {
|
||||
console.log("[connectMcp] ❌ no projectDir");
|
||||
setMcpStatus(t("mcp.pick_workspace_first", currentLocale()));
|
||||
return;
|
||||
}
|
||||
|
||||
const activeClient = client();
|
||||
console.log("[connectMcp] activeClient:", activeClient ? "exists" : "null");
|
||||
if (!activeClient) {
|
||||
@@ -3196,9 +3131,10 @@ export default function App() {
|
||||
|
||||
|
||||
onMount(async () => {
|
||||
const modePref = readModePreference();
|
||||
if (modePref) {
|
||||
setRememberModeChoice(true);
|
||||
const startupPref = readStartupPreference();
|
||||
if (startupPref) {
|
||||
setRememberStartupChoice(true);
|
||||
setStartupPreference(startupPref);
|
||||
}
|
||||
|
||||
const unsubscribeTheme = subscribeToSystemTheme((isDark) => {
|
||||
@@ -3566,7 +3502,7 @@ export default function App() {
|
||||
|
||||
createEffect(() => {
|
||||
if (!isTauriRuntime()) return;
|
||||
if (onboardingStep() !== "host") return;
|
||||
if (onboardingStep() !== "local") return;
|
||||
void workspaceStore.refreshEngineDoctor();
|
||||
});
|
||||
|
||||
@@ -3797,11 +3733,10 @@ export default function App() {
|
||||
});
|
||||
|
||||
const onboardingProps = () => ({
|
||||
mode: mode(),
|
||||
startupPreference: startupPreference(),
|
||||
onboardingStep: onboardingStep(),
|
||||
rememberModeChoice: rememberModeChoice(),
|
||||
rememberStartupChoice: rememberStartupChoice(),
|
||||
busy: busy(),
|
||||
baseUrl: baseUrl(),
|
||||
clientDirectory: clientDirectory(),
|
||||
openworkHostUrl: openworkServerSettings().urlOverride ?? "",
|
||||
openworkToken: openworkServerSettings().token ?? "",
|
||||
@@ -3824,7 +3759,6 @@ export default function App() {
|
||||
engineInstallLogs: engineInstallLogs(),
|
||||
error: error(),
|
||||
isWindows: isWindowsPlatform(),
|
||||
onBaseUrlChange: setBaseUrl,
|
||||
onClientDirectoryChange: setClientDirectory,
|
||||
onOpenworkHostUrlChange: (value: string) =>
|
||||
updateOpenworkServerSettings({
|
||||
@@ -3836,17 +3770,8 @@ export default function App() {
|
||||
...openworkServerSettings(),
|
||||
token: value,
|
||||
}),
|
||||
onModeSelect: (nextMode: Mode) => {
|
||||
if (nextMode === "host" && rememberModeChoice()) {
|
||||
writeModePreference("host");
|
||||
}
|
||||
if (nextMode === "client" && rememberModeChoice()) {
|
||||
writeModePreference("client");
|
||||
}
|
||||
setMode(nextMode);
|
||||
setOnboardingStep(nextMode === "host" ? "host" : "client");
|
||||
},
|
||||
onRememberModeToggle: () => setRememberModeChoice((v) => !v),
|
||||
onSelectStartup: workspaceStore.onSelectStartup,
|
||||
onRememberStartupToggle: workspaceStore.onRememberStartupToggle,
|
||||
onStartHost: workspaceStore.onStartHost,
|
||||
onCreateWorkspace: workspaceStore.createWorkspaceFlow,
|
||||
onPickWorkspaceFolder: workspaceStore.pickWorkspaceFolder,
|
||||
@@ -3854,7 +3779,7 @@ export default function App() {
|
||||
importingWorkspaceConfig: workspaceStore.importingWorkspaceConfig(),
|
||||
onAttachHost: workspaceStore.onAttachHost,
|
||||
onConnectClient: workspaceStore.onConnectClient,
|
||||
onBackToMode: workspaceStore.onBackToMode,
|
||||
onBackToWelcome: workspaceStore.onBackToWelcome,
|
||||
onSetAuthorizedDir: workspaceStore.setNewAuthorizedDir,
|
||||
onAddAuthorizedDir: workspaceStore.addAuthorizedDir,
|
||||
onAddAuthorizedDirFromPicker: () =>
|
||||
@@ -3926,7 +3851,7 @@ export default function App() {
|
||||
submitProviderApiKey,
|
||||
view: currentView(),
|
||||
setView,
|
||||
mode: mode(),
|
||||
startupPreference: startupPreference(),
|
||||
baseUrl: baseUrl(),
|
||||
clientConnected: Boolean(client()),
|
||||
busy: busy(),
|
||||
@@ -4065,7 +3990,11 @@ export default function App() {
|
||||
stopHost,
|
||||
openResetModal,
|
||||
resetModalBusy: resetModalBusy(),
|
||||
onResetStartupPreference: () => clearModePreference(),
|
||||
onResetStartupPreference: () => {
|
||||
clearStartupPreference();
|
||||
setStartupPreference(null);
|
||||
setRememberStartupChoice(false);
|
||||
},
|
||||
themeMode: themeMode(),
|
||||
setThemeMode,
|
||||
pendingPermissions: pendingPermissions(),
|
||||
@@ -4140,7 +4069,6 @@ export default function App() {
|
||||
activeWorkspaceRoot: workspaceStore.activeWorkspaceRoot().trim(),
|
||||
setWorkspaceSearch: workspaceStore.setWorkspaceSearch,
|
||||
setWorkspacePickerOpen: workspaceStore.setWorkspacePickerOpen,
|
||||
mode: mode(),
|
||||
clientConnected: Boolean(client()),
|
||||
openworkServerStatus: openworkServerStatus(),
|
||||
stopHost,
|
||||
|
||||
@@ -121,7 +121,7 @@ export function createCommandState(options: {
|
||||
|
||||
if (isRemoteWorkspace) {
|
||||
if (draft.scope !== "workspace") {
|
||||
options.setError("Global commands are only available in Host mode.");
|
||||
options.setError("Global commands are only available for local workspaces.");
|
||||
return;
|
||||
}
|
||||
if (!openworkClient || !openworkWorkspaceId || !openworkCapabilities?.commands?.write) {
|
||||
@@ -242,7 +242,7 @@ export function createCommandState(options: {
|
||||
|
||||
if (isRemoteWorkspace) {
|
||||
if (command.scope !== "workspace") {
|
||||
options.setError("Global commands are only available in Host mode.");
|
||||
options.setError("Global commands are only available for local workspaces.");
|
||||
return;
|
||||
}
|
||||
if (!openworkClient || !openworkWorkspaceId || !openworkCapabilities?.commands?.write) {
|
||||
|
||||
@@ -12,7 +12,6 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
onConfirm: (input: {
|
||||
openworkHostUrl?: string | null;
|
||||
openworkToken?: string | null;
|
||||
opencodeBaseUrl?: string | null;
|
||||
directory?: string | null;
|
||||
displayName?: string | null;
|
||||
}) => void;
|
||||
@@ -26,11 +25,9 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
let inputRef: HTMLInputElement | undefined;
|
||||
const translate = (key: string) => t(key, currentLocale());
|
||||
|
||||
const [remoteType, setRemoteType] = createSignal<"openwork" | "opencode">("opencode");
|
||||
const [openworkHostUrl, setOpenworkHostUrl] = createSignal("");
|
||||
const [openworkToken, setOpenworkToken] = createSignal("");
|
||||
const [openworkTokenVisible, setOpenworkTokenVisible] = createSignal(false);
|
||||
const [opencodeBaseUrl, setOpencodeBaseUrl] = createSignal("");
|
||||
const [directory, setDirectory] = createSignal("");
|
||||
const [displayName, setDisplayName] = createSignal("");
|
||||
|
||||
@@ -43,8 +40,7 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
|
||||
const canSubmit = createMemo(() => {
|
||||
if (submitting()) return false;
|
||||
if (remoteType() === "openwork") return openworkHostUrl().trim().length > 0;
|
||||
return opencodeBaseUrl().trim().length > 0;
|
||||
return openworkHostUrl().trim().length > 0;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
@@ -55,11 +51,9 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.open) return;
|
||||
setRemoteType("opencode");
|
||||
setOpenworkHostUrl("");
|
||||
setOpenworkToken("");
|
||||
setOpenworkTokenVisible(false);
|
||||
setOpencodeBaseUrl("");
|
||||
setDirectory("");
|
||||
setDisplayName("");
|
||||
});
|
||||
@@ -94,78 +88,38 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-2 rounded-xl border border-gray-6 bg-gray-1/60 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRemoteType("openwork")}
|
||||
class={`text-xs px-3 py-2 rounded-lg transition-colors ${
|
||||
remoteType() === "openwork"
|
||||
? "bg-gray-12/10 text-gray-12"
|
||||
: "text-gray-10 hover:text-gray-12"
|
||||
}`}
|
||||
>
|
||||
{translate("dashboard.remote_mode_openwork_alpha")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRemoteType("opencode")}
|
||||
class={`text-xs px-3 py-2 rounded-lg transition-colors ${
|
||||
remoteType() === "opencode"
|
||||
? "bg-gray-12/10 text-gray-12"
|
||||
: "text-gray-10 hover:text-gray-12"
|
||||
}`}
|
||||
>
|
||||
{translate("dashboard.remote_mode_direct")}
|
||||
</button>
|
||||
</div>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
label={translate("dashboard.openwork_host_label")}
|
||||
placeholder={translate("dashboard.openwork_host_placeholder")}
|
||||
value={openworkHostUrl()}
|
||||
onInput={(event) => setOpenworkHostUrl(event.currentTarget.value)}
|
||||
hint={translate("dashboard.openwork_host_hint")}
|
||||
disabled={submitting()}
|
||||
/>
|
||||
|
||||
<Show when={remoteType() === "openwork"}>
|
||||
<div class="space-y-4">
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
label={translate("dashboard.openwork_host_label")}
|
||||
placeholder={translate("dashboard.openwork_host_placeholder")}
|
||||
value={openworkHostUrl()}
|
||||
onInput={(event) => setOpenworkHostUrl(event.currentTarget.value)}
|
||||
hint={translate("dashboard.openwork_host_hint")}
|
||||
<label class="block">
|
||||
<div class="mb-1 text-xs font-medium text-gray-11">{translate("dashboard.openwork_host_token_label")}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type={openworkTokenVisible() ? "text" : "password"}
|
||||
value={openworkToken()}
|
||||
onInput={(event) => setOpenworkToken(event.currentTarget.value)}
|
||||
placeholder={translate("dashboard.openwork_host_token_placeholder")}
|
||||
disabled={submitting()}
|
||||
class="w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20"
|
||||
/>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 text-xs font-medium text-gray-11">{translate("dashboard.openwork_host_token_label")}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type={openworkTokenVisible() ? "text" : "password"}
|
||||
value={openworkToken()}
|
||||
onInput={(event) => setOpenworkToken(event.currentTarget.value)}
|
||||
placeholder={translate("dashboard.openwork_host_token_placeholder")}
|
||||
disabled={submitting()}
|
||||
class="w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-9 px-3 shrink-0"
|
||||
onClick={() => setOpenworkTokenVisible((prev) => !prev)}
|
||||
disabled={submitting()}
|
||||
>
|
||||
{openworkTokenVisible() ? translate("common.hide") : translate("common.show")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-10">{translate("dashboard.openwork_host_token_hint")}</div>
|
||||
</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-9 px-3 shrink-0"
|
||||
onClick={() => setOpenworkTokenVisible((prev) => !prev)}
|
||||
disabled={submitting()}
|
||||
>
|
||||
{openworkTokenVisible() ? translate("common.hide") : translate("common.show")}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={remoteType() === "opencode"}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
label={translate("dashboard.remote_base_url_label")}
|
||||
placeholder={translate("dashboard.remote_base_url_placeholder")}
|
||||
value={opencodeBaseUrl()}
|
||||
onInput={(event) => setOpencodeBaseUrl(event.currentTarget.value)}
|
||||
disabled={submitting()}
|
||||
/>
|
||||
</Show>
|
||||
<div class="mt-1 text-xs text-gray-10">{translate("dashboard.openwork_host_token_hint")}</div>
|
||||
</label>
|
||||
|
||||
<TextInput
|
||||
label={translate("dashboard.remote_directory_label")}
|
||||
@@ -194,23 +148,14 @@ export default function CreateRemoteWorkspaceModal(props: {
|
||||
<Button
|
||||
onClick={() =>
|
||||
props.onConfirm({
|
||||
openworkHostUrl: remoteType() === "openwork" ? openworkHostUrl().trim() : null,
|
||||
openworkToken: remoteType() === "openwork" ? openworkToken().trim() : null,
|
||||
opencodeBaseUrl: remoteType() === "opencode" ? opencodeBaseUrl().trim() : null,
|
||||
openworkHostUrl: openworkHostUrl().trim(),
|
||||
openworkToken: openworkToken().trim(),
|
||||
directory: directory().trim() ? directory().trim() : null,
|
||||
displayName: displayName().trim() ? displayName().trim() : null,
|
||||
})
|
||||
}
|
||||
disabled={!canSubmit()}
|
||||
title={
|
||||
remoteType() === "openwork"
|
||||
? !openworkHostUrl().trim()
|
||||
? translate("dashboard.remote_base_url_required")
|
||||
: undefined
|
||||
: !opencodeBaseUrl().trim()
|
||||
? translate("dashboard.remote_base_url_required")
|
||||
: undefined
|
||||
}
|
||||
title={!openworkHostUrl().trim() ? translate("dashboard.remote_base_url_required") : undefined}
|
||||
>
|
||||
{confirmLabel()}
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { applyEdits, modify } from "jsonc-parser";
|
||||
import { join } from "@tauri-apps/api/path";
|
||||
import { currentLocale, t } from "../../i18n";
|
||||
|
||||
import type { Client, Mode, PluginScope, ReloadReason, ReloadTrigger, SkillCard } from "../types";
|
||||
import type { Client, PluginScope, ReloadReason, ReloadTrigger, SkillCard } from "../types";
|
||||
import { addOpencodeCacheHint, isTauriRuntime } from "../utils";
|
||||
import skillCreatorTemplate from "../data/skill-creator.md?raw";
|
||||
import {
|
||||
@@ -33,7 +33,6 @@ export type ExtensionsStore = ReturnType<typeof createExtensionsStore>;
|
||||
|
||||
export function createExtensionsStore(options: {
|
||||
client: () => Client | null;
|
||||
mode: () => Mode | null;
|
||||
projectDir: () => string;
|
||||
activeWorkspaceRoot: () => string;
|
||||
workspaceType: () => "local" | "remote";
|
||||
@@ -85,7 +84,7 @@ export function createExtensionsStore(options: {
|
||||
async function refreshSkills(optionsOverride?: { force?: boolean }) {
|
||||
const root = options.activeWorkspaceRoot().trim();
|
||||
const isRemoteWorkspace = options.workspaceType() === "remote";
|
||||
const isHostMode = options.mode() === "host";
|
||||
const isLocalWorkspace = options.workspaceType() === "local";
|
||||
const openworkClient = options.openworkServerClient();
|
||||
const openworkWorkspaceId = options.openworkServerWorkspaceId();
|
||||
const openworkCapabilities = options.openworkServerCapabilities();
|
||||
@@ -101,8 +100,8 @@ export function createExtensionsStore(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
// Host mode or remote workspace: prefer OpenWork server when available
|
||||
if ((isRemoteWorkspace || isHostMode) && canUseOpenworkServer) {
|
||||
// Prefer OpenWork server when available
|
||||
if (canUseOpenworkServer) {
|
||||
if (root !== skillsRoot) {
|
||||
skillsLoaded = false;
|
||||
}
|
||||
@@ -121,7 +120,7 @@ export function createExtensionsStore(options: {
|
||||
try {
|
||||
setSkillsStatus(null);
|
||||
const response = await openworkClient.listSkills(openworkWorkspaceId, {
|
||||
includeGlobal: isHostMode,
|
||||
includeGlobal: isLocalWorkspace,
|
||||
});
|
||||
if (refreshSkillsAborted) return;
|
||||
const next: SkillCard[] = Array.isArray(response.items)
|
||||
@@ -151,7 +150,7 @@ export function createExtensionsStore(options: {
|
||||
|
||||
// Host/Tauri mode fallback: read directly from `.opencode/skills` or `.claude/skills`
|
||||
// so the UI still works even if the OpenCode engine is stopped or unreachable.
|
||||
if (isHostMode && isTauriRuntime()) {
|
||||
if (isLocalWorkspace && isTauriRuntime()) {
|
||||
if (root !== skillsRoot) {
|
||||
skillsLoaded = false;
|
||||
}
|
||||
@@ -201,11 +200,7 @@ export function createExtensionsStore(options: {
|
||||
const c = options.client();
|
||||
if (!c) {
|
||||
setSkills([]);
|
||||
setSkillsStatus(
|
||||
isRemoteWorkspace
|
||||
? "OpenWork server unavailable. Connect to load skills."
|
||||
: translate("skills.connect_host_to_load"),
|
||||
);
|
||||
setSkillsStatus("OpenWork server unavailable. Connect to load skills.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -274,7 +269,7 @@ export function createExtensionsStore(options: {
|
||||
|
||||
async function refreshPlugins(scopeOverride?: PluginScope) {
|
||||
const isRemoteWorkspace = options.workspaceType() === "remote";
|
||||
const isHostMode = options.mode() === "host";
|
||||
const isLocalWorkspace = options.workspaceType() === "local";
|
||||
const openworkClient = options.openworkServerClient();
|
||||
const openworkWorkspaceId = options.openworkServerWorkspaceId();
|
||||
const openworkCapabilities = options.openworkServerCapabilities();
|
||||
@@ -295,60 +290,18 @@ export function createExtensionsStore(options: {
|
||||
const scope = scopeOverride ?? pluginScope();
|
||||
const targetDir = options.projectDir().trim();
|
||||
|
||||
// Remote workspace: use OpenWork server
|
||||
if (isRemoteWorkspace) {
|
||||
setPluginConfig(null);
|
||||
setPluginConfigPath("opencode.json (remote)");
|
||||
if (scope !== "project") {
|
||||
setPluginStatus("Global plugins are only available in Host mode.");
|
||||
setPluginList([]);
|
||||
setSidebarPluginStatus("Switch to project scope to view remote plugins.");
|
||||
setSidebarPluginList([]);
|
||||
refreshPluginsInFlight = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canUseOpenworkServer) {
|
||||
setPluginStatus("OpenWork server unavailable. Plugins are read-only.");
|
||||
setPluginList([]);
|
||||
setSidebarPluginStatus("Connect OpenWork server to load plugins.");
|
||||
setSidebarPluginList([]);
|
||||
refreshPluginsInFlight = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setPluginStatus(null);
|
||||
setSidebarPluginStatus(null);
|
||||
|
||||
const result = await openworkClient.listPlugins(openworkWorkspaceId);
|
||||
if (refreshPluginsAborted) return;
|
||||
|
||||
const configItems = result.items.filter((item) => item.source === "config");
|
||||
const list = configItems.map((item) => item.spec);
|
||||
setPluginList(list);
|
||||
setSidebarPluginList(list);
|
||||
|
||||
if (!list.length) {
|
||||
setPluginStatus("No plugins configured yet.");
|
||||
}
|
||||
} catch (e) {
|
||||
if (refreshPluginsAborted) return;
|
||||
setPluginList([]);
|
||||
setSidebarPluginStatus("Failed to load plugins.");
|
||||
setSidebarPluginList([]);
|
||||
setPluginStatus(e instanceof Error ? e.message : "Failed to load plugins.");
|
||||
} finally {
|
||||
refreshPluginsInFlight = false;
|
||||
}
|
||||
|
||||
if (scope !== "project" && !isLocalWorkspace) {
|
||||
setPluginStatus("Global plugins are only available for local workspaces.");
|
||||
setPluginList([]);
|
||||
setSidebarPluginStatus("Global plugins require a local workspace.");
|
||||
setSidebarPluginList([]);
|
||||
refreshPluginsInFlight = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Host mode with OpenWork server: prefer server for project scope plugins
|
||||
if (isHostMode && scope === "project" && canUseOpenworkServer) {
|
||||
if (scope === "project" && canUseOpenworkServer) {
|
||||
setPluginConfig(null);
|
||||
setPluginConfigPath("opencode.json (openwork server)");
|
||||
setPluginConfigPath(`opencode.json (${isRemoteWorkspace ? "remote" : "openwork"} server)`);
|
||||
|
||||
try {
|
||||
setPluginStatus(null);
|
||||
@@ -356,10 +309,10 @@ export function createExtensionsStore(options: {
|
||||
|
||||
if (refreshPluginsAborted) return;
|
||||
|
||||
const result = await openworkClient.listPlugins(openworkWorkspaceId);
|
||||
const result = await openworkClient.listPlugins(openworkWorkspaceId, { includeGlobal: false });
|
||||
if (refreshPluginsAborted) return;
|
||||
|
||||
const configItems = result.items.filter((item) => item.source === "config");
|
||||
const configItems = result.items.filter((item) => item.source === "config" && item.scope === "project");
|
||||
const list = configItems.map((item) => item.spec);
|
||||
setPluginList(list);
|
||||
setSidebarPluginList(list);
|
||||
@@ -389,6 +342,15 @@ export function createExtensionsStore(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLocalWorkspace && !canUseOpenworkServer) {
|
||||
setPluginStatus("OpenWork server unavailable. Connect to manage plugins.");
|
||||
setPluginList([]);
|
||||
setSidebarPluginStatus("Connect an OpenWork server to load plugins.");
|
||||
setSidebarPluginList([]);
|
||||
refreshPluginsInFlight = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope === "project" && !targetDir) {
|
||||
setPluginStatus(translate("skills.pick_project_for_plugins"));
|
||||
setPluginList([]);
|
||||
@@ -447,7 +409,7 @@ export function createExtensionsStore(options: {
|
||||
const triggerName = stripPluginVersion(pluginName);
|
||||
|
||||
const isRemoteWorkspace = options.workspaceType() === "remote";
|
||||
const isHostMode = options.mode() === "host";
|
||||
const isLocalWorkspace = options.workspaceType() === "local";
|
||||
const openworkClient = options.openworkServerClient();
|
||||
const openworkWorkspaceId = options.openworkServerWorkspaceId();
|
||||
const openworkCapabilities = options.openworkServerCapabilities();
|
||||
@@ -464,31 +426,12 @@ export function createExtensionsStore(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRemoteWorkspace) {
|
||||
if (pluginScope() !== "project") {
|
||||
setPluginStatus("Global plugins are only available in Host mode.");
|
||||
return;
|
||||
}
|
||||
if (!canUseOpenworkServer) {
|
||||
setPluginStatus("OpenWork server unavailable. Connect to add plugins.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setPluginStatus(null);
|
||||
await openworkClient.addPlugin(openworkWorkspaceId, pluginName);
|
||||
if (isManualInput) {
|
||||
setPluginInput("");
|
||||
}
|
||||
await refreshPlugins("project");
|
||||
} catch (e) {
|
||||
setPluginStatus(e instanceof Error ? e.message : "Failed to add plugin.");
|
||||
}
|
||||
if (pluginScope() !== "project" && !isLocalWorkspace) {
|
||||
setPluginStatus("Global plugins are only available for local workspaces.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Host mode with OpenWork server: use server for project scope
|
||||
if (isHostMode && pluginScope() === "project" && canUseOpenworkServer) {
|
||||
if (pluginScope() === "project" && canUseOpenworkServer) {
|
||||
try {
|
||||
setPluginStatus(null);
|
||||
await openworkClient.addPlugin(openworkWorkspaceId, pluginName);
|
||||
@@ -507,6 +450,11 @@ export function createExtensionsStore(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLocalWorkspace && !canUseOpenworkServer) {
|
||||
setPluginStatus("OpenWork server unavailable. Connect to manage plugins.");
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = pluginScope();
|
||||
const targetDir = options.projectDir().trim();
|
||||
|
||||
@@ -560,8 +508,15 @@ export function createExtensionsStore(options: {
|
||||
}
|
||||
|
||||
async function importLocalSkill() {
|
||||
if (options.mode() !== "host" || !isTauriRuntime()) {
|
||||
options.setError(translate("skills.import_host_only"));
|
||||
const isLocalWorkspace = options.workspaceType() === "local";
|
||||
|
||||
if (!isTauriRuntime()) {
|
||||
options.setError(translate("skills.desktop_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLocalWorkspace) {
|
||||
options.setError("Local workspaces are required to import skills.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -607,7 +562,7 @@ export function createExtensionsStore(options: {
|
||||
|
||||
async function installSkillCreator() {
|
||||
const isRemoteWorkspace = options.workspaceType() === "remote";
|
||||
const isHostMode = options.mode() === "host";
|
||||
const isLocalWorkspace = options.workspaceType() === "local";
|
||||
const openworkClient = options.openworkServerClient();
|
||||
const openworkWorkspaceId = options.openworkServerWorkspaceId();
|
||||
const openworkCapabilities = options.openworkServerCapabilities();
|
||||
@@ -617,8 +572,8 @@ export function createExtensionsStore(options: {
|
||||
openworkWorkspaceId &&
|
||||
openworkCapabilities?.skills?.write;
|
||||
|
||||
// Remote workspace or host mode with server: use OpenWork server
|
||||
if ((isRemoteWorkspace || isHostMode) && canUseOpenworkServer) {
|
||||
// Use OpenWork server when available
|
||||
if (canUseOpenworkServer) {
|
||||
options.setBusy(true);
|
||||
options.setError(null);
|
||||
setSkillsStatus(translate("skills.installing_skill_creator"));
|
||||
@@ -650,8 +605,8 @@ export function createExtensionsStore(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.mode() !== "host") {
|
||||
options.setError(translate("skills.host_only_error"));
|
||||
if (!isLocalWorkspace) {
|
||||
options.setError("Local workspaces are required to install skills.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -729,8 +684,8 @@ export function createExtensionsStore(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.mode() !== "host") {
|
||||
options.setError(translate("skills.host_only_error"));
|
||||
if (options.workspaceType() !== "local") {
|
||||
options.setError("Local workspaces are required to uninstall skills.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,22 @@ import { createMemo, createSignal } from "solid-js";
|
||||
|
||||
import type {
|
||||
Client,
|
||||
Mode,
|
||||
StartupPreference,
|
||||
OnboardingStep,
|
||||
WorkspaceDisplay,
|
||||
WorkspaceOpenworkConfig,
|
||||
WorkspacePreset,
|
||||
EngineRuntime,
|
||||
} from "../types";
|
||||
import { addOpencodeCacheHint, isTauriRuntime, normalizeDirectoryPath, safeStringify, writeModePreference } from "../utils";
|
||||
import {
|
||||
addOpencodeCacheHint,
|
||||
clearStartupPreference,
|
||||
isTauriRuntime,
|
||||
normalizeDirectoryPath,
|
||||
readStartupPreference,
|
||||
safeStringify,
|
||||
writeStartupPreference,
|
||||
} from "../utils";
|
||||
import { unwrap } from "../lib/opencode";
|
||||
import {
|
||||
createOpenworkServerClient,
|
||||
@@ -53,11 +61,12 @@ import { mapConfigProvidersToList } from "../utils/providers";
|
||||
export type WorkspaceStore = ReturnType<typeof createWorkspaceStore>;
|
||||
|
||||
export function createWorkspaceStore(options: {
|
||||
mode: () => Mode | null;
|
||||
setMode: (mode: Mode | null) => void;
|
||||
startupPreference: () => StartupPreference | null;
|
||||
setStartupPreference: (value: StartupPreference | null) => void;
|
||||
onboardingStep: () => OnboardingStep;
|
||||
setOnboardingStep: (step: OnboardingStep) => void;
|
||||
rememberModeChoice: () => boolean;
|
||||
rememberStartupChoice: () => boolean;
|
||||
setRememberStartupChoice: (value: boolean) => void;
|
||||
baseUrl: () => string;
|
||||
setBaseUrl: (value: string) => void;
|
||||
clientDirectory: () => string;
|
||||
@@ -198,33 +207,38 @@ export function createWorkspaceStore(options: {
|
||||
} catch (error) {
|
||||
if (error instanceof OpenworkServerError && (error.status === 401 || error.status === 403)) {
|
||||
if (!trimmedToken) {
|
||||
throw new Error("Access token required for OpenWork host.");
|
||||
throw new Error("Access token required for OpenWork server.");
|
||||
}
|
||||
throw new Error("OpenWork host rejected the client token.");
|
||||
throw new Error("OpenWork server rejected the access token.");
|
||||
}
|
||||
return { kind: "fallback" as const };
|
||||
}
|
||||
|
||||
if (!trimmedToken) {
|
||||
throw new Error("Access token required for OpenWork host.");
|
||||
throw new Error("Access token required for OpenWork server.");
|
||||
}
|
||||
|
||||
const response = await client.listWorkspaces();
|
||||
const items = Array.isArray(response.items) ? response.items : [];
|
||||
const workspace = items[0] as OpenworkWorkspaceInfo | undefined;
|
||||
if (!workspace) {
|
||||
throw new Error("OpenWork host did not return a workspace.");
|
||||
throw new Error("OpenWork server did not return a workspace.");
|
||||
}
|
||||
let opencodeBaseUrl = workspace.opencode?.baseUrl?.trim() ?? workspace.baseUrl?.trim() ?? "";
|
||||
if (!opencodeBaseUrl) {
|
||||
throw new Error("OpenWork host did not provide an OpenCode URL.");
|
||||
throw new Error("OpenWork server did not provide an OpenCode URL.");
|
||||
}
|
||||
|
||||
const opencodeUsername = workspace.opencode?.username?.trim() ?? "";
|
||||
const opencodePassword = workspace.opencode?.password?.trim() ?? "";
|
||||
const opencodeAuth =
|
||||
let opencodeAuth: OpencodeAuth | undefined =
|
||||
opencodeUsername && opencodePassword ? { username: opencodeUsername, password: opencodePassword } : undefined;
|
||||
|
||||
if (!isTauriRuntime()) {
|
||||
opencodeBaseUrl = `${normalized.replace(/\/+$/, "")}/opencode`;
|
||||
opencodeAuth = trimmedToken ? { token: trimmedToken, mode: "openwork" } : undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const hostUrl = new URL(normalized);
|
||||
const opencodeUrl = new URL(opencodeBaseUrl);
|
||||
@@ -288,7 +302,8 @@ export function createWorkspaceStore(options: {
|
||||
const info = await engineInfo();
|
||||
setEngine(info);
|
||||
|
||||
const syncLocalState = options.mode() !== "client";
|
||||
const isRemoteWorkspace = activeWorkspaceInfo()?.workspaceType === "remote";
|
||||
const syncLocalState = !isRemoteWorkspace;
|
||||
|
||||
const username = info.opencodeUsername?.trim() ?? "";
|
||||
const password = info.opencodePassword?.trim() ?? "";
|
||||
@@ -304,7 +319,6 @@ export function createWorkspaceStore(options: {
|
||||
|
||||
if (
|
||||
syncLocalState &&
|
||||
options.mode() === "host" &&
|
||||
info.running &&
|
||||
info.baseUrl &&
|
||||
!options.client() &&
|
||||
@@ -362,12 +376,12 @@ export function createWorkspaceStore(options: {
|
||||
|
||||
try {
|
||||
if (isRemote) {
|
||||
options.setMode("client");
|
||||
options.setStartupPreference("server");
|
||||
|
||||
if (remoteType === "openwork") {
|
||||
const hostUrl = next.openworkHostUrl?.trim() ?? "";
|
||||
if (!hostUrl) {
|
||||
options.setError("OpenWork host URL is required.");
|
||||
options.setError("OpenWork server URL is required.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -492,7 +506,8 @@ export function createWorkspaceStore(options: {
|
||||
return true;
|
||||
}
|
||||
|
||||
const wasHostMode = options.mode() === "host" && options.client();
|
||||
const wasLocalConnection = options.startupPreference() === "local" && options.client();
|
||||
options.setStartupPreference("local");
|
||||
const nextRoot = isRemote ? next.directory?.trim() ?? "" : next.path;
|
||||
const oldWorkspacePath = projectDir();
|
||||
const workspaceChanged = oldWorkspacePath !== nextRoot;
|
||||
@@ -544,7 +559,7 @@ export function createWorkspaceStore(options: {
|
||||
await options.loadCommands({ workspaceRoot: next.path }).catch(() => undefined);
|
||||
}
|
||||
|
||||
if (!isRemote && workspaceChanged && options.client() && !wasHostMode) {
|
||||
if (!isRemote && workspaceChanged && options.client() && !wasLocalConnection) {
|
||||
options.setSelectedSessionId(null);
|
||||
options.setMessages([]);
|
||||
options.setTodos([]);
|
||||
@@ -553,8 +568,8 @@ export function createWorkspaceStore(options: {
|
||||
await options.loadSessions(next.path).catch(() => undefined);
|
||||
}
|
||||
|
||||
// In Host mode, restart the engine when workspace changes
|
||||
if (!isRemote && wasHostMode && workspaceChanged) {
|
||||
// When running locally, restart the engine when workspace changes
|
||||
if (!isRemote && wasLocalConnection && workspaceChanged) {
|
||||
options.setError(null);
|
||||
options.setBusy(true);
|
||||
options.setBusyLabel("status.restarting_engine");
|
||||
@@ -829,62 +844,56 @@ export function createWorkspaceStore(options: {
|
||||
async function createRemoteWorkspaceFlow(input: {
|
||||
openworkHostUrl?: string | null;
|
||||
openworkToken?: string | null;
|
||||
opencodeBaseUrl?: string | null;
|
||||
directory?: string | null;
|
||||
displayName?: string | null;
|
||||
}) {
|
||||
const hostUrl = normalizeOpenworkServerUrl(input.openworkHostUrl ?? "") ?? "";
|
||||
const token = input.openworkToken?.trim() ?? "";
|
||||
const directBaseUrl = input.opencodeBaseUrl?.trim() ?? "";
|
||||
const directory = input.directory?.trim() ?? "";
|
||||
const displayName = input.displayName?.trim() || null;
|
||||
|
||||
if (!hostUrl && !directBaseUrl) {
|
||||
options.setError("Add an OpenWork host URL or a direct OpenCode URL to continue.");
|
||||
if (!hostUrl) {
|
||||
options.setError(t("app.error.remote_base_url_required", currentLocale()));
|
||||
return false;
|
||||
}
|
||||
|
||||
options.setError(null);
|
||||
console.log("[workspace] create remote", {
|
||||
hostUrl: hostUrl || null,
|
||||
baseUrl: directBaseUrl || null,
|
||||
directory: directory || null,
|
||||
displayName,
|
||||
});
|
||||
|
||||
options.setMode("client");
|
||||
options.setStartupPreference("server");
|
||||
|
||||
let remoteType: "openwork" | "opencode" = "opencode";
|
||||
let resolvedBaseUrl = directBaseUrl;
|
||||
let remoteType: "openwork" = "openwork";
|
||||
let resolvedBaseUrl = "";
|
||||
let resolvedDirectory = directory;
|
||||
let openworkWorkspace: OpenworkWorkspaceInfo | null = null;
|
||||
let resolvedAuth: OpencodeAuth | undefined = undefined;
|
||||
let resolvedHostUrl = hostUrl;
|
||||
|
||||
if (hostUrl) {
|
||||
options.updateOpenworkServerSettings({
|
||||
...options.openworkServerSettings(),
|
||||
urlOverride: hostUrl,
|
||||
token: token || undefined,
|
||||
});
|
||||
options.updateOpenworkServerSettings({
|
||||
...options.openworkServerSettings(),
|
||||
urlOverride: hostUrl,
|
||||
token: token || undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const resolved = await resolveOpenworkHost({ hostUrl, token });
|
||||
if (resolved.kind === "openwork") {
|
||||
remoteType = "openwork";
|
||||
resolvedBaseUrl = resolved.opencodeBaseUrl;
|
||||
resolvedDirectory = resolved.directory || directory;
|
||||
openworkWorkspace = resolved.workspace;
|
||||
resolvedHostUrl = resolved.hostUrl;
|
||||
resolvedAuth = resolved.auth;
|
||||
} else if (!resolvedBaseUrl) {
|
||||
resolvedBaseUrl = hostUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : safeStringify(error);
|
||||
options.setError(addOpencodeCacheHint(message));
|
||||
try {
|
||||
const resolved = await resolveOpenworkHost({ hostUrl, token });
|
||||
if (resolved.kind !== "openwork") {
|
||||
options.setError("OpenWork server unavailable. Check the URL and token.");
|
||||
return false;
|
||||
}
|
||||
resolvedBaseUrl = resolved.opencodeBaseUrl;
|
||||
resolvedDirectory = resolved.directory || directory;
|
||||
openworkWorkspace = resolved.workspace;
|
||||
resolvedHostUrl = resolved.hostUrl;
|
||||
resolvedAuth = resolved.auth;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : safeStringify(error);
|
||||
options.setError(addOpencodeCacheHint(message));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!resolvedBaseUrl) {
|
||||
@@ -1243,8 +1252,8 @@ export function createWorkspaceStore(options: {
|
||||
options.setSessionStatusById({});
|
||||
options.setSseConnected(false);
|
||||
|
||||
options.setMode(null);
|
||||
options.setOnboardingStep("mode");
|
||||
options.setStartupPreference(null);
|
||||
options.setOnboardingStep("welcome");
|
||||
|
||||
options.setView("session");
|
||||
} catch (e) {
|
||||
@@ -1263,8 +1272,8 @@ export function createWorkspaceStore(options: {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.mode() !== "host") {
|
||||
options.setError("Reload is only available in Host mode.");
|
||||
if (activeWorkspaceDisplay().workspaceType !== "local") {
|
||||
options.setError("Reload is only available for local workspaces.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1501,13 +1510,7 @@ export function createWorkspaceStore(options: {
|
||||
}
|
||||
|
||||
async function bootstrapOnboarding() {
|
||||
const modePref = (() => {
|
||||
try {
|
||||
return window.localStorage.getItem("openwork.modePref") as Mode | null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const startupPref = readStartupPreference();
|
||||
const onboardingComplete = (() => {
|
||||
try {
|
||||
return window.localStorage.getItem("openwork.onboardingComplete") === "1";
|
||||
@@ -1568,29 +1571,38 @@ export function createWorkspaceStore(options: {
|
||||
|
||||
const activeWorkspace = activeWorkspaceInfo();
|
||||
if (activeWorkspace?.workspaceType === "remote") {
|
||||
options.setMode("client");
|
||||
options.setStartupPreference("server");
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await activateWorkspace(activeWorkspace.id);
|
||||
if (!ok) {
|
||||
options.setOnboardingStep("client");
|
||||
options.setOnboardingStep("server");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modePref && activeWorkspacePath().trim()) {
|
||||
options.setMode("host");
|
||||
if (startupPref) {
|
||||
options.setStartupPreference(startupPref);
|
||||
}
|
||||
|
||||
if (startupPref === "server") {
|
||||
options.setOnboardingStep("server");
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeWorkspacePath().trim()) {
|
||||
options.setStartupPreference("local");
|
||||
|
||||
if (info?.running && info.baseUrl) {
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await connectToServer(
|
||||
info.baseUrl,
|
||||
info.projectDir ?? undefined,
|
||||
{ reason: "bootstrap-host" },
|
||||
{ reason: "bootstrap-local" },
|
||||
engineAuth() ?? undefined,
|
||||
);
|
||||
if (!ok) {
|
||||
options.setMode(null);
|
||||
options.setOnboardingStep("mode");
|
||||
options.setStartupPreference(null);
|
||||
options.setOnboardingStep("welcome");
|
||||
return;
|
||||
}
|
||||
markOnboardingComplete();
|
||||
@@ -1600,135 +1612,85 @@ export function createWorkspaceStore(options: {
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await startHost({ workspacePath: activeWorkspacePath().trim() });
|
||||
if (!ok) {
|
||||
options.setOnboardingStep("host");
|
||||
options.setOnboardingStep("local");
|
||||
return;
|
||||
}
|
||||
markOnboardingComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modePref) return;
|
||||
|
||||
if (modePref === "host") {
|
||||
options.setMode("host");
|
||||
|
||||
if (info?.running && info.baseUrl) {
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await connectToServer(
|
||||
info.baseUrl,
|
||||
info.projectDir ?? undefined,
|
||||
{ reason: "bootstrap-host" },
|
||||
engineAuth() ?? undefined,
|
||||
);
|
||||
if (!ok) {
|
||||
options.setMode(null);
|
||||
options.setOnboardingStep("mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTauriRuntime() && activeWorkspacePath().trim()) {
|
||||
if (!authorizedDirs().length && activeWorkspacePath().trim()) {
|
||||
setAuthorizedDirs([activeWorkspacePath().trim()]);
|
||||
}
|
||||
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await startHost({ workspacePath: activeWorkspacePath().trim() });
|
||||
if (!ok) {
|
||||
options.setOnboardingStep("host");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
options.setOnboardingStep("host");
|
||||
if (startupPref === "local") {
|
||||
options.setOnboardingStep("local");
|
||||
return;
|
||||
}
|
||||
|
||||
options.setMode("client");
|
||||
if (!options.baseUrl().trim()) {
|
||||
options.setOnboardingStep("client");
|
||||
return;
|
||||
}
|
||||
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await connectToServer(
|
||||
options.baseUrl().trim(),
|
||||
options.clientDirectory().trim() ? options.clientDirectory().trim() : undefined,
|
||||
{ reason: "client-connect" },
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
options.setOnboardingStep("client");
|
||||
}
|
||||
options.setOnboardingStep("welcome");
|
||||
}
|
||||
|
||||
function onModeSelect(nextMode: Mode) {
|
||||
if (nextMode === "host" && options.rememberModeChoice()) {
|
||||
writeModePreference("host");
|
||||
function onSelectStartup(nextPref: StartupPreference) {
|
||||
if (options.rememberStartupChoice()) {
|
||||
writeStartupPreference(nextPref);
|
||||
}
|
||||
if (nextMode === "client" && options.rememberModeChoice()) {
|
||||
writeModePreference("client");
|
||||
}
|
||||
options.setMode(nextMode);
|
||||
options.setOnboardingStep(nextMode === "host" ? "host" : "client");
|
||||
options.setStartupPreference(nextPref);
|
||||
options.setOnboardingStep(nextPref === "local" ? "local" : "server");
|
||||
}
|
||||
|
||||
function onBackToMode() {
|
||||
options.setMode(null);
|
||||
options.setOnboardingStep("mode");
|
||||
function onBackToWelcome() {
|
||||
options.setStartupPreference(null);
|
||||
options.setOnboardingStep("welcome");
|
||||
}
|
||||
|
||||
async function onStartHost() {
|
||||
options.setMode("host");
|
||||
options.setStartupPreference("local");
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await startHost({ workspacePath: activeWorkspacePath().trim() });
|
||||
if (!ok) {
|
||||
options.setOnboardingStep("host");
|
||||
options.setOnboardingStep("local");
|
||||
}
|
||||
}
|
||||
|
||||
async function onAttachHost() {
|
||||
options.setMode("host");
|
||||
options.setStartupPreference("local");
|
||||
options.setOnboardingStep("connecting");
|
||||
const ok = await connectToServer(
|
||||
engine()?.baseUrl ?? "",
|
||||
engine()?.projectDir ?? undefined,
|
||||
{ reason: "attach-host" },
|
||||
{ reason: "attach-local" },
|
||||
engineAuth() ?? undefined,
|
||||
);
|
||||
if (!ok) {
|
||||
options.setMode(null);
|
||||
options.setOnboardingStep("mode");
|
||||
options.setStartupPreference(null);
|
||||
options.setOnboardingStep("welcome");
|
||||
}
|
||||
}
|
||||
|
||||
async function onConnectClient() {
|
||||
options.setMode("client");
|
||||
options.setStartupPreference("server");
|
||||
options.setOnboardingStep("connecting");
|
||||
const settings = options.openworkServerSettings();
|
||||
const ok = await createRemoteWorkspaceFlow({
|
||||
openworkHostUrl: settings.urlOverride ?? null,
|
||||
openworkToken: settings.token ?? null,
|
||||
opencodeBaseUrl: options.baseUrl().trim(),
|
||||
directory: options.clientDirectory().trim() ? options.clientDirectory().trim() : null,
|
||||
displayName: null,
|
||||
});
|
||||
if (!ok) {
|
||||
options.setOnboardingStep("client");
|
||||
options.setOnboardingStep("server");
|
||||
}
|
||||
}
|
||||
|
||||
function onRememberModeToggle() {
|
||||
function onRememberStartupToggle() {
|
||||
if (typeof window === "undefined") return;
|
||||
const next = !options.rememberModeChoice();
|
||||
const next = !options.rememberStartupChoice();
|
||||
options.setRememberStartupChoice(next);
|
||||
try {
|
||||
if (next) {
|
||||
const current = options.mode();
|
||||
if (current === "host" || current === "client") {
|
||||
writeModePreference(current);
|
||||
const current = options.startupPreference();
|
||||
if (current === "local" || current === "server") {
|
||||
writeStartupPreference(current);
|
||||
}
|
||||
} else {
|
||||
window.localStorage.removeItem("openwork.modePref");
|
||||
clearStartupPreference();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -1783,12 +1745,12 @@ export function createWorkspaceStore(options: {
|
||||
stopHost,
|
||||
reloadWorkspaceEngine,
|
||||
bootstrapOnboarding,
|
||||
onModeSelect,
|
||||
onBackToMode,
|
||||
onSelectStartup,
|
||||
onBackToWelcome,
|
||||
onStartHost,
|
||||
onAttachHost,
|
||||
onConnectClient,
|
||||
onRememberModeToggle,
|
||||
onRememberStartupToggle,
|
||||
onInstallEngine,
|
||||
addAuthorizedDir,
|
||||
addAuthorizedDirFromPicker,
|
||||
|
||||
@@ -10,6 +10,8 @@ type FieldsResult<T> =
|
||||
export type OpencodeAuth = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
mode?: "basic" | "openwork";
|
||||
};
|
||||
|
||||
const encodeBasicAuth = (auth?: OpencodeAuth) => {
|
||||
@@ -21,11 +23,19 @@ const encodeBasicAuth = (auth?: OpencodeAuth) => {
|
||||
return buffer ? buffer.from(token, "utf8").toString("base64") : null;
|
||||
};
|
||||
|
||||
const createTauriFetch = (auth?: OpencodeAuth) => {
|
||||
const resolveAuthHeader = (auth?: OpencodeAuth) => {
|
||||
if (auth?.mode === "openwork" && auth.token) {
|
||||
return `Bearer ${auth.token}`;
|
||||
}
|
||||
const encoded = encodeBasicAuth(auth);
|
||||
return encoded ? `Basic ${encoded}` : null;
|
||||
};
|
||||
|
||||
const createTauriFetch = (auth?: OpencodeAuth) => {
|
||||
const authHeader = resolveAuthHeader(auth);
|
||||
const addAuth = (headers: Headers) => {
|
||||
if (!encoded || headers.has("Authorization")) return;
|
||||
headers.set("Authorization", `Basic ${encoded}`);
|
||||
if (!authHeader || headers.has("Authorization")) return;
|
||||
headers.set("Authorization", authHeader);
|
||||
};
|
||||
|
||||
return (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
@@ -61,9 +71,9 @@ export function unwrap<T>(result: FieldsResult<T>): NonNullable<T> {
|
||||
export function createClient(baseUrl: string, directory?: string, auth?: OpencodeAuth) {
|
||||
const headers: Record<string, string> = {};
|
||||
if (!isTauriRuntime()) {
|
||||
const encoded = encodeBasicAuth(auth);
|
||||
if (encoded) {
|
||||
headers.Authorization = `Basic ${encoded}`;
|
||||
const authHeader = resolveAuthHeader(auth);
|
||||
if (authHeader) {
|
||||
headers.Authorization = authHeader;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
SettingsTab,
|
||||
ScheduledJob,
|
||||
SkillCard,
|
||||
StartupPreference,
|
||||
WorkspaceCommand,
|
||||
View,
|
||||
} from "../types";
|
||||
@@ -63,7 +64,7 @@ export type DashboardViewProps = {
|
||||
submitProviderApiKey: (providerId: string, apiKey: string) => Promise<string | void>;
|
||||
view: View;
|
||||
setView: (view: View, sessionId?: string) => void;
|
||||
mode: "host" | "client" | null;
|
||||
startupPreference: StartupPreference | null;
|
||||
baseUrl: string;
|
||||
clientConnected: boolean;
|
||||
busy: boolean;
|
||||
@@ -84,6 +85,7 @@ export type DashboardViewProps = {
|
||||
openworkAuditError: string | null;
|
||||
opencodeConnectStatus: OpencodeConnectStatus | null;
|
||||
engineInfo: EngineInfo | null;
|
||||
engineDoctorVersion: string | null;
|
||||
openwrkStatus: OpenwrkStatus | null;
|
||||
owpenbotInfo: OwpenbotInfo | null;
|
||||
updateOpenworkServerSettings: (next: OpenworkServerSettings) => void;
|
||||
@@ -436,7 +438,12 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
<Show when={props.mode === "client" && props.openworkServerStatus === "disconnected"}>
|
||||
<Show
|
||||
when={
|
||||
props.activeWorkspaceDisplay.workspaceType === "remote" &&
|
||||
props.openworkServerStatus === "disconnected"
|
||||
}
|
||||
>
|
||||
<div class="text-[11px] text-gray-9 px-1">
|
||||
OpenWork server is offline — remote tasks still run.
|
||||
</div>
|
||||
@@ -842,7 +849,6 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
<Match when={props.tab === "skills"}>
|
||||
<SkillsView
|
||||
busy={props.busy}
|
||||
mode={props.mode}
|
||||
canInstallSkillCreator={props.canInstallSkillCreator}
|
||||
canUseDesktopTools={props.canUseDesktopTools}
|
||||
accessHint={props.skillsAccessHint}
|
||||
@@ -881,7 +887,6 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
|
||||
<Match when={props.tab === "mcp"}>
|
||||
<McpView
|
||||
mode={props.mode}
|
||||
busy={props.busy}
|
||||
activeWorkspaceRoot={props.activeWorkspaceRoot}
|
||||
mcpServers={props.mcpServers}
|
||||
@@ -901,7 +906,7 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
|
||||
<Match when={props.tab === "settings"}>
|
||||
<SettingsView
|
||||
mode={props.mode}
|
||||
startupPreference={props.startupPreference}
|
||||
baseUrl={props.baseUrl}
|
||||
headerStatus={props.headerStatus}
|
||||
busy={props.busy}
|
||||
@@ -925,6 +930,7 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
engineInfo={props.engineInfo}
|
||||
openwrkStatus={props.openwrkStatus}
|
||||
owpenbotInfo={props.owpenbotInfo}
|
||||
engineDoctorVersion={props.engineDoctorVersion}
|
||||
updateOpenworkServerSettings={props.updateOpenworkServerSettings}
|
||||
resetOpenworkServerSettings={props.resetOpenworkServerSettings}
|
||||
testOpenworkServerConnection={props.testOpenworkServerConnection}
|
||||
|
||||
@@ -24,7 +24,6 @@ import TextInput from "../components/text-input";
|
||||
import { currentLocale, t, type Language } from "../../i18n";
|
||||
|
||||
export type McpViewProps = {
|
||||
mode: "host" | "client" | null;
|
||||
busy: boolean;
|
||||
activeWorkspaceRoot: string;
|
||||
mcpServers: McpServerEntry[];
|
||||
@@ -178,8 +177,7 @@ export default function McpView(props: McpViewProps) {
|
||||
return status?.status === "connected";
|
||||
};
|
||||
|
||||
const canConnect = (entry: McpDirectoryInfo) =>
|
||||
props.mode === "host" && isTauriRuntime() && !props.busy && !!props.activeWorkspaceRoot.trim();
|
||||
const canConnect = () => !props.busy;
|
||||
|
||||
return (
|
||||
<section class="space-y-6">
|
||||
@@ -263,7 +261,7 @@ export default function McpView(props: McpViewProps) {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => props.connectMcp(entry)}
|
||||
disabled={!canConnect(entry) || props.mcpConnectingName === entry.name}
|
||||
disabled={!canConnect() || props.mcpConnectingName === entry.name}
|
||||
>
|
||||
{props.mcpConnectingName === entry.name ? (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { For, Match, Show, Switch, createSignal } from "solid-js";
|
||||
import type { Mode, OnboardingStep } from "../types";
|
||||
import type { OnboardingStep, StartupPreference } from "../types";
|
||||
import type { WorkspaceInfo } from "../lib/tauri";
|
||||
import { CheckCircle2, ChevronDown, Circle, Globe } from "lucide-solid";
|
||||
|
||||
@@ -11,11 +11,10 @@ import { isTauriRuntime, isWindowsPlatform } from "../utils/index";
|
||||
import { currentLocale, t } from "../../i18n";
|
||||
|
||||
export type OnboardingViewProps = {
|
||||
mode: Mode | null;
|
||||
startupPreference: StartupPreference | null;
|
||||
onboardingStep: OnboardingStep;
|
||||
rememberModeChoice: boolean;
|
||||
rememberStartupChoice: boolean;
|
||||
busy: boolean;
|
||||
baseUrl: string;
|
||||
clientDirectory: string;
|
||||
openworkHostUrl: string;
|
||||
openworkToken: string;
|
||||
@@ -38,12 +37,11 @@ export type OnboardingViewProps = {
|
||||
error: string | null;
|
||||
developerMode: boolean;
|
||||
isWindows: boolean;
|
||||
onBaseUrlChange: (value: string) => void;
|
||||
onClientDirectoryChange: (value: string) => void;
|
||||
onOpenworkHostUrlChange: (value: string) => void;
|
||||
onOpenworkTokenChange: (value: string) => void;
|
||||
onModeSelect: (mode: Mode) => void;
|
||||
onRememberModeToggle: () => void;
|
||||
onSelectStartup: (mode: StartupPreference) => void;
|
||||
onRememberStartupToggle: () => void;
|
||||
onStartHost: () => void;
|
||||
onCreateWorkspace: (preset: "starter" | "automation" | "minimal", folder: string | null) => void;
|
||||
onPickWorkspaceFolder: () => Promise<string | null>;
|
||||
@@ -51,7 +49,7 @@ export type OnboardingViewProps = {
|
||||
importingWorkspaceConfig: boolean;
|
||||
onAttachHost: () => void;
|
||||
onConnectClient: () => void;
|
||||
onBackToMode: () => void;
|
||||
onBackToWelcome: () => void;
|
||||
onSetAuthorizedDir: (value: string) => void;
|
||||
onAddAuthorizedDir: () => void;
|
||||
onAddAuthorizedDirFromPicker: () => void;
|
||||
@@ -103,10 +101,12 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h2 class="text-xl font-medium mb-2">
|
||||
{props.mode === "host" ? translate("onboarding.starting_host") : translate("onboarding.searching_host")}
|
||||
{props.startupPreference === "local"
|
||||
? translate("onboarding.starting_host")
|
||||
: translate("onboarding.searching_host")}
|
||||
</h2>
|
||||
<p class="text-gray-10 text-sm">
|
||||
{props.mode === "host"
|
||||
{props.startupPreference === "local"
|
||||
? translate("onboarding.getting_ready")
|
||||
: translate("onboarding.verifying")}
|
||||
</p>
|
||||
@@ -116,7 +116,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={props.onboardingStep === "host"}>
|
||||
<Match when={props.onboardingStep === "local"}>
|
||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gray-1 text-gray-12 p-6 relative">
|
||||
<div class="absolute top-0 left-0 w-full h-96 bg-gradient-to-b from-gray-2 to-transparent opacity-20 pointer-events-none" />
|
||||
|
||||
@@ -222,7 +222,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
{translate("onboarding.start")}
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" onClick={props.onBackToMode} disabled={props.busy} class="w-full">
|
||||
<Button variant="ghost" onClick={props.onBackToWelcome} disabled={props.busy} class="w-full">
|
||||
{translate("onboarding.back")}
|
||||
</Button>
|
||||
|
||||
@@ -422,7 +422,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={props.onboardingStep === "client"}>
|
||||
<Match when={props.onboardingStep === "server"}>
|
||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gray-1 text-gray-12 p-6 relative">
|
||||
<div class="absolute top-0 left-0 w-full h-96 bg-gradient-to-b from-gray-2 to-transparent opacity-20 pointer-events-none" />
|
||||
|
||||
@@ -447,11 +447,34 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
|
||||
<div class="space-y-4">
|
||||
<TextInput
|
||||
label={translate("dashboard.remote_base_url_label")}
|
||||
placeholder={translate("dashboard.remote_base_url_placeholder")}
|
||||
value={props.baseUrl}
|
||||
onInput={(e) => props.onBaseUrlChange(e.currentTarget.value)}
|
||||
label={translate("dashboard.openwork_host_label")}
|
||||
placeholder={translate("dashboard.openwork_host_placeholder")}
|
||||
value={props.openworkHostUrl}
|
||||
onInput={(e) => props.onOpenworkHostUrlChange(e.currentTarget.value)}
|
||||
hint={translate("dashboard.openwork_host_hint")}
|
||||
/>
|
||||
<label class="block">
|
||||
<div class="mb-1 text-xs font-medium text-gray-11">{translate("dashboard.openwork_host_token_label")}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type={openworkTokenVisible() ? "text" : "password"}
|
||||
value={props.openworkToken}
|
||||
onInput={(e) => props.onOpenworkTokenChange(e.currentTarget.value)}
|
||||
placeholder={translate("dashboard.openwork_host_token_placeholder")}
|
||||
disabled={props.busy}
|
||||
class="w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-9 px-3 shrink-0"
|
||||
onClick={() => setOpenworkTokenVisible((prev) => !prev)}
|
||||
disabled={props.busy}
|
||||
>
|
||||
{openworkTokenVisible() ? translate("common.hide") : translate("common.show")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-10">{translate("dashboard.openwork_host_token_hint")}</div>
|
||||
</label>
|
||||
<TextInput
|
||||
label={translate("dashboard.remote_directory_label")}
|
||||
placeholder={translate("dashboard.remote_directory_placeholder")}
|
||||
@@ -461,55 +484,15 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details class="rounded-2xl border border-gray-6 bg-gray-1/60 px-4 py-3">
|
||||
<summary class="flex items-center justify-between cursor-pointer text-xs text-gray-10">
|
||||
{translate("onboarding.advanced_openwork_host")}
|
||||
<ChevronDown size={14} class="text-gray-7" />
|
||||
</summary>
|
||||
<div class="pt-3 space-y-3">
|
||||
<div class="text-xs text-gray-10">{translate("onboarding.advanced_openwork_hint")}</div>
|
||||
<TextInput
|
||||
label={translate("dashboard.openwork_host_label")}
|
||||
placeholder={translate("dashboard.openwork_host_placeholder")}
|
||||
value={props.openworkHostUrl}
|
||||
onInput={(e) => props.onOpenworkHostUrlChange(e.currentTarget.value)}
|
||||
hint={translate("dashboard.openwork_host_hint")}
|
||||
/>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 text-xs font-medium text-gray-11">{translate("dashboard.openwork_host_token_label")}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type={openworkTokenVisible() ? "text" : "password"}
|
||||
value={props.openworkToken}
|
||||
onInput={(e) => props.onOpenworkTokenChange(e.currentTarget.value)}
|
||||
placeholder={translate("dashboard.openwork_host_token_placeholder")}
|
||||
disabled={props.busy}
|
||||
class="w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-9 px-3 shrink-0"
|
||||
onClick={() => setOpenworkTokenVisible((prev) => !prev)}
|
||||
disabled={props.busy}
|
||||
>
|
||||
{openworkTokenVisible() ? translate("common.hide") : translate("common.show")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-10">{translate("dashboard.openwork_host_token_hint")}</div>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<Button
|
||||
onClick={props.onConnectClient}
|
||||
disabled={props.busy || (!props.baseUrl.trim() && !props.openworkHostUrl.trim())}
|
||||
disabled={props.busy || !props.openworkHostUrl.trim()}
|
||||
class="w-full py-3 text-base"
|
||||
>
|
||||
{translate("onboarding.remote_workspace_action")}
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" onClick={props.onBackToMode} disabled={props.busy} class="w-full">
|
||||
<Button variant="ghost" onClick={props.onBackToWelcome} disabled={props.busy} class="w-full">
|
||||
{translate("onboarding.back")}
|
||||
</Button>
|
||||
|
||||
@@ -540,7 +523,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
|
||||
<div class="space-y-4">
|
||||
<button
|
||||
onClick={() => props.onModeSelect("host")}
|
||||
onClick={() => props.onSelectStartup("local")}
|
||||
class="group w-full relative bg-gray-2 hover:bg-gray-4 border border-gray-6 hover:border-gray-7 p-6 md:p-8 rounded-3xl text-left transition-all duration-300 hover:shadow-2xl hover:shadow-indigo-6/10 hover:-translate-y-0.5 flex items-start gap-6"
|
||||
>
|
||||
<div class="shrink-0 w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-7/20 to-purple-7/20 flex items-center justify-center border border-indigo-7/20 group-hover:border-indigo-7/40 transition-colors">
|
||||
@@ -578,7 +561,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
</Show>
|
||||
|
||||
<button
|
||||
onClick={() => props.onModeSelect("client")}
|
||||
onClick={() => props.onSelectStartup("server")}
|
||||
class="group w-full relative bg-gray-2 hover:bg-gray-4 border border-gray-6 hover:border-gray-7 p-6 md:p-8 rounded-3xl text-left transition-all duration-300 hover:shadow-2xl hover:shadow-gray-12/10 hover:-translate-y-0.5 flex items-start gap-6"
|
||||
>
|
||||
<div class="shrink-0 w-14 h-14 rounded-2xl bg-gradient-to-br from-gray-7/20 to-gray-5/10 flex items-center justify-center border border-gray-6 group-hover:border-gray-7 transition-colors">
|
||||
@@ -596,17 +579,17 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
|
||||
<div class="flex items-center gap-2 px-2 py-1">
|
||||
<button
|
||||
onClick={props.onRememberModeToggle}
|
||||
onClick={props.onRememberStartupToggle}
|
||||
class="flex items-center gap-2 text-xs text-gray-10 hover:text-gray-11 transition-colors group"
|
||||
>
|
||||
<div
|
||||
class={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
|
||||
props.rememberModeChoice
|
||||
props.rememberStartupChoice
|
||||
? "bg-indigo-7 border-indigo-7 text-gray-12"
|
||||
: "border-gray-7 bg-transparent group-hover:border-gray-7"
|
||||
}`}
|
||||
>
|
||||
<Show when={props.rememberModeChoice}>
|
||||
<Show when={props.rememberStartupChoice}>
|
||||
<CheckCircle2 size={10} />
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -75,22 +75,22 @@ export default function ScheduledTasksView(props: ScheduledTasksViewProps) {
|
||||
});
|
||||
const sourceDescription = createMemo(() =>
|
||||
props.source === "remote"
|
||||
? "Automations that run on a schedule from the connected OpenWork host."
|
||||
? "Automations that run on a schedule from the connected OpenWork server."
|
||||
: "Automations that run on a schedule from this device."
|
||||
);
|
||||
const sourceLabel = createMemo(() =>
|
||||
props.source === "remote" ? "From OpenWork host" : "From local scheduler"
|
||||
props.source === "remote" ? "From OpenWork server" : "From local scheduler"
|
||||
);
|
||||
const schedulerLabel = createMemo(() => (props.source === "remote" ? "OpenWork host" : "Local"));
|
||||
const schedulerLabel = createMemo(() => (props.source === "remote" ? "OpenWork server" : "Local"));
|
||||
const schedulerHint = createMemo(() =>
|
||||
props.source === "remote" ? "Remote instance" : "Launchd or systemd"
|
||||
);
|
||||
const schedulerUnavailableHint = createMemo(() =>
|
||||
props.source === "remote" ? "OpenWork host unavailable" : "Desktop-only"
|
||||
props.source === "remote" ? "OpenWork server unavailable" : "Desktop-only"
|
||||
);
|
||||
const deleteDescription = createMemo(() =>
|
||||
props.source === "remote"
|
||||
? "This removes the schedule and deletes the job definition from the connected OpenWork host."
|
||||
? "This removes the schedule and deletes the job definition from the connected OpenWork server."
|
||||
: "This removes the schedule and deletes the job definition from your machine."
|
||||
);
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export type SessionViewProps = {
|
||||
activeWorkspaceRoot: string;
|
||||
setWorkspaceSearch: (value: string) => void;
|
||||
setWorkspacePickerOpen: (open: boolean) => void;
|
||||
mode: "host" | "client" | null;
|
||||
clientConnected: boolean;
|
||||
openworkServerStatus: OpenworkServerStatus;
|
||||
stopHost: () => void;
|
||||
|
||||
@@ -6,7 +6,7 @@ import Button from "../components/button";
|
||||
import TextInput from "../components/text-input";
|
||||
import SettingsKeybinds, { type KeybindSetting } from "../components/settings-keybinds";
|
||||
import { ChevronDown, HardDrive, MessageCircle, PlugZap, RefreshCcw, Shield, Smartphone, X } from "lucide-solid";
|
||||
import type { OpencodeConnectStatus, ProviderListItem, SettingsTab } from "../types";
|
||||
import type { OpencodeConnectStatus, ProviderListItem, SettingsTab, StartupPreference } from "../types";
|
||||
import { createOpenworkServerClient } from "../lib/openwork-server";
|
||||
import type {
|
||||
OpenworkAuditEntry,
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
} from "../lib/tauri";
|
||||
|
||||
export type SettingsViewProps = {
|
||||
mode: "host" | "client" | null;
|
||||
startupPreference: StartupPreference | null;
|
||||
baseUrl: string;
|
||||
headerStatus: string;
|
||||
busy: boolean;
|
||||
@@ -127,7 +127,6 @@ export type SettingsViewProps = {
|
||||
// Owpenbot Settings Component
|
||||
function OwpenbotSettings(props: {
|
||||
busy: boolean;
|
||||
mode: "host" | "client" | null;
|
||||
openworkServerStatus: OpenworkServerStatus;
|
||||
openworkServerUrl: string;
|
||||
openworkServerSettings: OpenworkServerSettings;
|
||||
@@ -157,7 +156,7 @@ function OwpenbotSettings(props: {
|
||||
const hostToken = props.openworkServerHostInfo?.hostToken?.trim() ?? "";
|
||||
const clientToken = props.openworkServerHostInfo?.clientToken?.trim() ?? "";
|
||||
const settingsToken = props.openworkServerSettings.token?.trim() ?? "";
|
||||
const token = props.mode === "host" ? clientToken : settingsToken;
|
||||
const token = clientToken || settingsToken;
|
||||
if (!baseUrl || !token || !props.openworkServerWorkspaceId) return null;
|
||||
return createOpenworkServerClient({ baseUrl, token, hostToken });
|
||||
});
|
||||
@@ -310,9 +309,10 @@ function OwpenbotSettings(props: {
|
||||
setOwpenbotStatus(latestStatus);
|
||||
}
|
||||
const serverClient = openworkServerClient();
|
||||
const useRemote = Boolean(serverClient && props.openworkServerWorkspaceId);
|
||||
const workspaceId = props.openworkServerWorkspaceId;
|
||||
const useRemote = Boolean(serverClient && workspaceId);
|
||||
debugOwpenbot("save-token:start", {
|
||||
mode: props.mode ?? "unknown",
|
||||
connection: props.openworkServerHostInfo ? "local" : "remote",
|
||||
tauri: isTauriRuntime(),
|
||||
useRemote,
|
||||
openworkServerStatus: props.openworkServerStatus,
|
||||
@@ -320,13 +320,10 @@ function OwpenbotSettings(props: {
|
||||
openworkServerWorkspaceId: props.openworkServerWorkspaceId,
|
||||
owpenbotHealthPort: latestStatus?.healthPort ?? owpenbotStatus()?.healthPort ?? null,
|
||||
hasToken: Boolean(
|
||||
(props.mode === "host"
|
||||
? props.openworkServerHostInfo?.clientToken?.trim()
|
||||
: props.openworkServerSettings.token?.trim())
|
||||
?? false,
|
||||
(props.openworkServerHostInfo?.clientToken?.trim() || props.openworkServerSettings.token?.trim()) ?? false,
|
||||
),
|
||||
});
|
||||
if (useRemote) {
|
||||
if (useRemote && serverClient && workspaceId) {
|
||||
if (props.openworkServerStatus === "disconnected") {
|
||||
setTelegramFeedback(
|
||||
"error",
|
||||
@@ -344,7 +341,7 @@ function OwpenbotSettings(props: {
|
||||
setTelegramFeedback("checking", "Saving token on the host...");
|
||||
try {
|
||||
await serverClient.setOwpenbotTelegramToken(
|
||||
props.openworkServerWorkspaceId,
|
||||
workspaceId,
|
||||
token,
|
||||
latestStatus?.healthPort ?? owpenbotStatus()?.healthPort ?? null,
|
||||
);
|
||||
@@ -814,8 +811,6 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
const [openworkTokenVisible, setOpenworkTokenVisible] = createSignal(false);
|
||||
const [clientTokenVisible, setClientTokenVisible] = createSignal(false);
|
||||
const [hostTokenVisible, setHostTokenVisible] = createSignal(false);
|
||||
const [opencodeUserVisible, setOpencodeUserVisible] = createSignal(false);
|
||||
const [opencodePasswordVisible, setOpencodePasswordVisible] = createSignal(false);
|
||||
const [copyingField, setCopyingField] = createSignal<string | null>(null);
|
||||
let copyTimeout: number | undefined;
|
||||
|
||||
@@ -960,6 +955,14 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
return "bg-green-7/10 text-green-11 border-green-7/20";
|
||||
});
|
||||
|
||||
const isLocalEngineRunning = createMemo(() => Boolean(props.engineInfo?.running));
|
||||
const isLocalPreference = createMemo(() => props.startupPreference === "local");
|
||||
const startupLabel = createMemo(() => {
|
||||
if (props.startupPreference === "local") return "Start local server";
|
||||
if (props.startupPreference === "server") return "Connect to server";
|
||||
return "Not set";
|
||||
});
|
||||
|
||||
const tabLabel = (tab: SettingsTab) => {
|
||||
switch (tab) {
|
||||
case "model":
|
||||
@@ -1104,8 +1107,6 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
return info?.connectUrl ?? info?.mdnsUrl ?? info?.lanUrl ?? info?.baseUrl ?? "";
|
||||
});
|
||||
const hostConnectUrlUsesMdns = createMemo(() => hostConnectUrl().includes(".local"));
|
||||
const opencodeUsername = createMemo(() => props.engineInfo?.opencodeUsername?.trim() ?? "");
|
||||
const opencodePassword = createMemo(() => props.engineInfo?.opencodePassword?.trim() ?? "");
|
||||
|
||||
const handleCopy = async (value: string, field: string) => {
|
||||
if (!value) return;
|
||||
@@ -1198,14 +1199,14 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
<Shield size={16} />
|
||||
{props.developerMode ? "Disable Developer Mode" : "Enable Developer Mode"}
|
||||
</Button>
|
||||
<Show when={props.mode === "host"}>
|
||||
<Show when={isLocalEngineRunning()}>
|
||||
<Button variant="danger" onClick={props.stopHost} disabled={props.busy}>
|
||||
Stop engine
|
||||
Stop local server
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={props.mode === "client"}>
|
||||
<Show when={!isLocalEngineRunning() && props.openworkServerStatus === "connected"}>
|
||||
<Button variant="outline" onClick={props.stopHost} disabled={props.busy}>
|
||||
Disconnect
|
||||
Disconnect server
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -1448,14 +1449,14 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class={`p-2 rounded-lg ${
|
||||
props.mode === "host" ? "bg-indigo-7/10 text-indigo-11" : "bg-green-7/10 text-green-11"
|
||||
isLocalPreference() ? "bg-indigo-7/10 text-indigo-11" : "bg-green-7/10 text-green-11"
|
||||
}`}
|
||||
>
|
||||
<Show when={props.mode === "host"} fallback={<Smartphone size={18} />}>
|
||||
<Show when={isLocalPreference()} fallback={<Smartphone size={18} />}>
|
||||
<HardDrive size={18} />
|
||||
</Show>
|
||||
</div>
|
||||
<span class="capitalize text-sm font-medium text-gray-12">{props.mode} mode</span>
|
||||
<span class="text-sm font-medium text-gray-12">{startupLabel()}</span>
|
||||
</div>
|
||||
<Button variant="outline" class="text-xs h-8 py-0 px-3" onClick={props.stopHost} disabled={props.busy}>
|
||||
Switch
|
||||
@@ -1463,12 +1464,12 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
</div>
|
||||
|
||||
<Button variant="secondary" class="w-full justify-between group" onClick={props.onResetStartupPreference}>
|
||||
<span class="text-gray-11">Reset default startup mode</span>
|
||||
<span class="text-gray-11">Reset startup preference</span>
|
||||
<RefreshCcw size={14} class="text-gray-10 group-hover:rotate-180 transition-transform" />
|
||||
</Button>
|
||||
|
||||
<p class="text-xs text-gray-7">
|
||||
This clears your saved preference and shows mode selection on next launch.
|
||||
This clears your saved preference and shows the connection choice on next launch.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1478,7 +1479,7 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
<div class="text-xs text-gray-10">Power options for the engine and reset actions.</div>
|
||||
</div>
|
||||
|
||||
<Show when={isTauriRuntime() && props.mode === "host"}>
|
||||
<Show when={isTauriRuntime() && isLocalPreference()}>
|
||||
<div class="space-y-3">
|
||||
<div class="text-xs text-gray-10">Engine source</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@@ -1567,14 +1568,14 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
|
||||
<Match when={activeTab() === "remote"}>
|
||||
<div class="space-y-6">
|
||||
<Show when={props.mode === "host"}>
|
||||
<Show when={hostInfo()}>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">OpenWork host pairing</div>
|
||||
<div class="text-sm font-medium text-gray-12">OpenWork server sharing</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
Share these details with a trusted device. Keep the host on the same network for the fastest setup.
|
||||
Share these details with a trusted device. Keep the server on the same network for the fastest setup.
|
||||
</div>
|
||||
</div>
|
||||
<div class={`text-xs px-2 py-1 rounded-full border ${hostStatusStyle()}`}>
|
||||
@@ -1609,7 +1610,7 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
|
||||
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium text-gray-11">Client token</div>
|
||||
<div class="text-xs font-medium text-gray-11">Access token</div>
|
||||
<div class="text-xs text-gray-7 font-mono truncate">
|
||||
{clientTokenVisible()
|
||||
? hostInfo()?.clientToken || "—"
|
||||
@@ -1617,7 +1618,7 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
? "••••••••••••"
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-8 mt-1">Use on phones or laptops connecting to this host.</div>
|
||||
<div class="text-[11px] text-gray-8 mt-1">Use on phones or laptops connecting to this server.</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
@@ -1641,7 +1642,7 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
|
||||
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium text-gray-11">Host token</div>
|
||||
<div class="text-xs font-medium text-gray-11">Server token</div>
|
||||
<div class="text-xs text-gray-7 font-mono truncate">
|
||||
{hostTokenVisible()
|
||||
? hostInfo()?.hostToken || "—"
|
||||
@@ -1649,7 +1650,7 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
? "••••••••••••"
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-8 mt-1">Keep private. Required for host approvals.</div>
|
||||
<div class="text-[11px] text-gray-8 mt-1">Keep private. Required for approval actions.</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
@@ -1673,183 +1674,96 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">OpenCode direct access</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
Use these credentials when a client connects directly to OpenCode without an OpenWork host.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium text-gray-11">OpenCode username</div>
|
||||
<div class="text-xs text-gray-7 font-mono truncate">
|
||||
{opencodeUserVisible()
|
||||
? opencodeUsername() || "—"
|
||||
: opencodeUsername()
|
||||
? "••••••••"
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-8 mt-1">Use with the password when connecting directly.</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-8 py-0 px-3"
|
||||
onClick={() => setOpencodeUserVisible((prev) => !prev)}
|
||||
disabled={!opencodeUsername()}
|
||||
>
|
||||
{opencodeUserVisible() ? "Hide" : "Show"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-8 py-0 px-3"
|
||||
onClick={() => handleCopy(opencodeUsername(), "opencode-user")}
|
||||
disabled={!opencodeUsername()}
|
||||
>
|
||||
{copyingField() === "opencode-user" ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium text-gray-11">OpenCode password</div>
|
||||
<div class="text-xs text-gray-7 font-mono truncate">
|
||||
{opencodePasswordVisible()
|
||||
? opencodePassword() || "—"
|
||||
: opencodePassword()
|
||||
? "••••••••"
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-8 mt-1">Keep private. Required for direct OpenCode access.</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-8 py-0 px-3"
|
||||
onClick={() => setOpencodePasswordVisible((prev) => !prev)}
|
||||
disabled={!opencodePassword()}
|
||||
>
|
||||
{opencodePasswordVisible() ? "Hide" : "Show"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-8 py-0 px-3"
|
||||
onClick={() => handleCopy(opencodePassword(), "opencode-pass")}
|
||||
disabled={!opencodePassword()}
|
||||
>
|
||||
{copyingField() === "opencode-pass" ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.mode === "client"}>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sm font-medium text-gray-12">OpenWork host</div>
|
||||
<span class="text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-full bg-amber-7/10 text-amber-11 border border-amber-7/30">
|
||||
Alpha
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
Connect to a host running OpenWork on another device. Use the host URL and client token from pairing.
|
||||
</div>
|
||||
</div>
|
||||
<div class={`text-xs px-2 py-1 rounded-full border ${openworkStatusStyle()}`}>
|
||||
{openworkStatusLabel()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3">
|
||||
<TextInput
|
||||
label="OpenWork host URL"
|
||||
value={openworkUrl()}
|
||||
onInput={(event) => setOpenworkUrl(event.currentTarget.value)}
|
||||
placeholder="http://127.0.0.1:8787"
|
||||
hint="Use the host URL shared during pairing."
|
||||
disabled={props.busy}
|
||||
/>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 text-xs font-medium text-gray-11">Client token</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type={openworkTokenVisible() ? "text" : "password"}
|
||||
value={openworkToken()}
|
||||
onInput={(event) => setOpenworkToken(event.currentTarget.value)}
|
||||
placeholder="Paste your token"
|
||||
disabled={props.busy}
|
||||
class="w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-9 px-3 shrink-0"
|
||||
onClick={() => setOpenworkTokenVisible((prev) => !prev)}
|
||||
disabled={props.busy}
|
||||
>
|
||||
{openworkTokenVisible() ? "Hide" : "Show"}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-10">Optional. Paste the client token from the host to pair.</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-[11px] text-gray-7 font-mono truncate">
|
||||
Resolved host: {props.openworkServerUrl || "Not set"}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const next = buildOpenworkSettings();
|
||||
props.updateOpenworkServerSettings(next);
|
||||
await props.testOpenworkServerConnection(next);
|
||||
}}
|
||||
disabled={props.busy}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => props.updateOpenworkServerSettings(buildOpenworkSettings())}
|
||||
disabled={props.busy || !hasOpenworkChanges()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={props.resetOpenworkServerSettings}
|
||||
disabled={props.busy}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-3">
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-2/30 border border-gray-6/50 rounded-2xl p-5 space-y-4">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-12">OpenCode direct</div>
|
||||
<div class="text-sm font-medium text-gray-12">OpenWork server</div>
|
||||
<div class="text-xs text-gray-10">
|
||||
Use this only when no OpenWork host is available. Manage direct engine connections from the workspace picker.
|
||||
Connect to an OpenWork server. Use the URL and access token from your server admin.
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[11px] text-gray-7 font-mono truncate">
|
||||
Current engine: {props.baseUrl || "Not connected"}
|
||||
<div class={`text-xs px-2 py-1 rounded-full border ${openworkStatusStyle()}`}>
|
||||
{openworkStatusLabel()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3">
|
||||
<TextInput
|
||||
label="OpenWork server URL"
|
||||
value={openworkUrl()}
|
||||
onInput={(event) => setOpenworkUrl(event.currentTarget.value)}
|
||||
placeholder="http://127.0.0.1:8787"
|
||||
hint="Use the URL shared by your OpenWork server."
|
||||
disabled={props.busy}
|
||||
/>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 text-xs font-medium text-gray-11">Access token</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type={openworkTokenVisible() ? "text" : "password"}
|
||||
value={openworkToken()}
|
||||
onInput={(event) => setOpenworkToken(event.currentTarget.value)}
|
||||
placeholder="Paste your token"
|
||||
disabled={props.busy}
|
||||
class="w-full rounded-xl bg-gray-2/60 px-3 py-2 text-sm text-gray-12 placeholder:text-gray-10 shadow-[0_0_0_1px_rgba(255,255,255,0.08)] focus:outline-none focus:ring-2 focus:ring-gray-6/20"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="text-xs h-9 px-3 shrink-0"
|
||||
onClick={() => setOpenworkTokenVisible((prev) => !prev)}
|
||||
disabled={props.busy}
|
||||
>
|
||||
{openworkTokenVisible() ? "Hide" : "Show"}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-10">Optional. Paste the access token to authenticate.</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-[11px] text-gray-7 font-mono truncate">
|
||||
Resolved server: {openworkUrl().trim() || "Not set"}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const next = buildOpenworkSettings();
|
||||
props.updateOpenworkServerSettings(next);
|
||||
await props.testOpenworkServerConnection(next);
|
||||
}}
|
||||
disabled={props.busy}
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => props.updateOpenworkServerSettings(buildOpenworkSettings())}
|
||||
disabled={props.busy || !hasOpenworkChanges()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={props.resetOpenworkServerSettings}
|
||||
disabled={props.busy}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={openworkStatusLabel() !== "Connected"}>
|
||||
<div class="text-xs text-gray-9">
|
||||
OpenWork server connection needed to sync skills, plugins, and commands.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
@@ -1857,7 +1771,6 @@ export default function SettingsView(props: SettingsViewProps) {
|
||||
<div class="space-y-6">
|
||||
<OwpenbotSettings
|
||||
busy={props.busy}
|
||||
mode={props.mode}
|
||||
openworkServerStatus={props.openworkServerStatus}
|
||||
openworkServerUrl={props.openworkServerUrl}
|
||||
openworkServerSettings={props.openworkServerSettings}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { currentLocale, t } from "../../i18n";
|
||||
|
||||
export type SkillsViewProps = {
|
||||
busy: boolean;
|
||||
mode: "host" | "client" | null;
|
||||
canInstallSkillCreator: boolean;
|
||||
canUseDesktopTools: boolean;
|
||||
accessHint?: string | null;
|
||||
@@ -56,7 +55,6 @@ export default function SkillsView(props: SkillsViewProps) {
|
||||
<Show
|
||||
when={
|
||||
!props.accessHint &&
|
||||
props.mode !== "host" &&
|
||||
!props.canInstallSkillCreator &&
|
||||
!props.canUseDesktopTools
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { relaunch } from "@tauri-apps/plugin-process";
|
||||
|
||||
import type {
|
||||
Client,
|
||||
Mode,
|
||||
PluginScope,
|
||||
ReloadReason,
|
||||
ReloadTrigger,
|
||||
@@ -32,7 +31,6 @@ export type NotionState = {
|
||||
|
||||
export function createSystemState(options: {
|
||||
client: Accessor<Client | null>;
|
||||
mode: Accessor<Mode | null>;
|
||||
sessions: Accessor<Session[]>;
|
||||
sessionStatusById: Accessor<Record<string, string>>;
|
||||
refreshPlugins: (scopeOverride?: PluginScope) => Promise<void>;
|
||||
@@ -210,9 +208,8 @@ export function createSystemState(options: {
|
||||
if (reloadBusy()) return false;
|
||||
const override = options.canReloadWorkspaceEngine?.();
|
||||
if (override === true) return true;
|
||||
if (!options.client()) return false;
|
||||
if (override === false) return false;
|
||||
if (options.mode() !== "host") return false;
|
||||
if (!options.client()) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -230,10 +227,6 @@ export function createSystemState(options: {
|
||||
setReloadError("Reload is unavailable for this workspace.");
|
||||
return;
|
||||
}
|
||||
if (override !== true && options.mode() !== "host") {
|
||||
setReloadError("Reload is only available in Host mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
// if (anyActiveRuns()) {
|
||||
// setReloadError("Waiting for active tasks to complete before reloading.");
|
||||
|
||||
@@ -91,11 +91,11 @@ export type OpencodeEvent = {
|
||||
|
||||
export type View = "onboarding" | "dashboard" | "session" | "proto";
|
||||
|
||||
export type Mode = "host" | "client";
|
||||
export type StartupPreference = "local" | "server";
|
||||
|
||||
export type EngineRuntime = "direct" | "openwrk";
|
||||
|
||||
export type OnboardingStep = "mode" | "host" | "client" | "connecting";
|
||||
export type OnboardingStep = "welcome" | "local" | "server" | "connecting";
|
||||
|
||||
export type DashboardTab =
|
||||
| "home"
|
||||
|
||||
@@ -85,23 +85,22 @@ export function isWindowsPlatform() {
|
||||
return /windows/i.test(platform) || /windows/i.test(ua);
|
||||
}
|
||||
|
||||
export function readModePreference(): "host" | "client" | null {
|
||||
const STARTUP_PREF_KEY = "openwork.startupPref";
|
||||
const LEGACY_PREF_KEY = "openwork.modePref";
|
||||
const LEGACY_PREF_KEY_ALT = "openwork_mode_pref";
|
||||
|
||||
export function readStartupPreference(): "local" | "server" | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
try {
|
||||
const pref =
|
||||
window.localStorage.getItem("openwork.modePref") ??
|
||||
window.localStorage.getItem("openwork_mode_pref");
|
||||
window.localStorage.getItem(STARTUP_PREF_KEY) ??
|
||||
window.localStorage.getItem(LEGACY_PREF_KEY) ??
|
||||
window.localStorage.getItem(LEGACY_PREF_KEY_ALT);
|
||||
|
||||
if (pref === "host" || pref === "client") {
|
||||
// Migrate legacy key if needed.
|
||||
try {
|
||||
window.localStorage.setItem("openwork.modePref", pref);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return pref;
|
||||
}
|
||||
if (pref === "local" || pref === "server") return pref;
|
||||
if (pref === "host") return "local";
|
||||
if (pref === "client") return "server";
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -109,24 +108,25 @@ export function readModePreference(): "host" | "client" | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function writeModePreference(nextMode: "host" | "client") {
|
||||
export function writeStartupPreference(nextPref: "local" | "server") {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
window.localStorage.setItem("openwork.modePref", nextMode);
|
||||
// Keep legacy key for now.
|
||||
window.localStorage.setItem("openwork_mode_pref", nextMode);
|
||||
window.localStorage.setItem(STARTUP_PREF_KEY, nextPref);
|
||||
window.localStorage.removeItem(LEGACY_PREF_KEY);
|
||||
window.localStorage.removeItem(LEGACY_PREF_KEY_ALT);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function clearModePreference() {
|
||||
export function clearStartupPreference() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
window.localStorage.removeItem("openwork.modePref");
|
||||
window.localStorage.removeItem("openwork_mode_pref");
|
||||
window.localStorage.removeItem(STARTUP_PREF_KEY);
|
||||
window.localStorage.removeItem(LEGACY_PREF_KEY);
|
||||
window.localStorage.removeItem(LEGACY_PREF_KEY_ALT);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -51,21 +51,21 @@ export default {
|
||||
"dashboard.create_workspace_subtitle": "Initialize a new folder-based workspace.",
|
||||
"dashboard.create_workspace_confirm": "Create Workspace",
|
||||
"dashboard.create_remote_workspace_title": "Add Remote Workspace",
|
||||
"dashboard.create_remote_workspace_subtitle": "Save an OpenWork host or OpenCode engine as a workspace.",
|
||||
"dashboard.create_remote_workspace_subtitle": "Save an OpenWork server as a workspace.",
|
||||
"dashboard.create_remote_workspace_confirm": "Add Workspace",
|
||||
"dashboard.remote_workspace_title": "Remote workspace",
|
||||
"dashboard.remote_workspace_hint": "Track an OpenWork host or OpenCode engine and reconnect anytime.",
|
||||
"dashboard.remote_base_url_label": "OpenCode URL",
|
||||
"dashboard.remote_base_url_placeholder": "http://127.0.0.1:4096",
|
||||
"dashboard.remote_workspace_hint": "Track an OpenWork server and reconnect anytime.",
|
||||
"dashboard.remote_base_url_label": "OpenWork server URL",
|
||||
"dashboard.remote_base_url_placeholder": "http://127.0.0.1:8787",
|
||||
"dashboard.remote_base_url_required": "Add a server URL to continue.",
|
||||
"dashboard.openwork_host_label": "OpenWork host URL",
|
||||
"dashboard.openwork_host_placeholder": "https://your-host.openwork.app",
|
||||
"dashboard.openwork_host_hint": "Use the URL shared by your OpenWork host.",
|
||||
"dashboard.openwork_host_token_label": "Client token",
|
||||
"dashboard.openwork_host_label": "OpenWork server URL",
|
||||
"dashboard.openwork_host_placeholder": "https://your-server.openwork.app",
|
||||
"dashboard.openwork_host_hint": "Use the URL shared by your OpenWork server.",
|
||||
"dashboard.openwork_host_token_label": "Access token",
|
||||
"dashboard.openwork_host_token_placeholder": "Paste your token",
|
||||
"dashboard.openwork_host_token_hint": "Optional. Paste the client token from the host to pair.",
|
||||
"dashboard.remote_mode_openwork_alpha": "OpenWork host (alpha)",
|
||||
"dashboard.remote_mode_direct": "OpenCode direct (recommended)",
|
||||
"dashboard.openwork_host_token_hint": "Optional. Paste the access token from the server to authenticate.",
|
||||
"dashboard.remote_mode_openwork_alpha": "OpenWork server",
|
||||
"dashboard.remote_mode_direct": "Direct (legacy)",
|
||||
"dashboard.remote_connection_openwork": "OpenWork",
|
||||
"dashboard.remote_connection_direct": "Direct",
|
||||
"dashboard.remote_directory_label": "Workspace directory (optional)",
|
||||
@@ -197,7 +197,7 @@ export default {
|
||||
"skills.add_title": "Add skills",
|
||||
"skills.add_description": "Install a starter command, import a skill, or open the folder.",
|
||||
"skills.install_from_openpackage": "Install from OpenPackage",
|
||||
"skills.host_mode_only": "Host mode only",
|
||||
"skills.host_mode_only": "Local workspace only",
|
||||
"skills.install": "Install",
|
||||
"skills.installed_label": "Installed",
|
||||
"skills.install_hint": "Installs OpenPackage packages into the current workspace. Skills should land in `.opencode/skills`.",
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
"skills.installed": "Installed skills",
|
||||
"skills.no_skills": "No skills detected in `.opencode/skills` or `.claude/skills`.",
|
||||
"skills.desktop_required": "Skill management requires the desktop app.",
|
||||
"skills.host_only_error": "Skill management is only available in Host mode.",
|
||||
"skills.host_only_error": "Skill management requires a local workspace or connected OpenWork server.",
|
||||
"skills.install_skill_creator": "Install skill creator",
|
||||
"skills.install_skill_creator_hint": "This skill allows you to create other skills from within the chat.",
|
||||
"skills.installing_skill_creator": "Installing skill creator...",
|
||||
@@ -233,13 +233,13 @@ export default {
|
||||
"skills.notion_crm_title": "Notion CRM Enrichment Skills",
|
||||
"skills.notion_crm_description": "Add enrichment workflows for contacts, pipelines, and follow-ups.",
|
||||
"skills.notion_crm_card_description": "Enrich Notion CRM data with ready-made skills.",
|
||||
"skills.connect_host_to_load": "Connect to a host to load skills.",
|
||||
"skills.connect_host_to_load": "Connect an OpenWork server to load skills.",
|
||||
"skills.pick_workspace_first": "Pick a workspace folder first.",
|
||||
"skills.no_skills_found": "No skills found yet.",
|
||||
"skills.installed_description": "Skills available in this workspace.",
|
||||
"skills.failed_to_load": "Failed to load skills",
|
||||
"skills.plugin_management_host_only": "Plugin management is only available in Host mode.",
|
||||
"skills.plugins_host_only": "Plugins are only available in Host mode.",
|
||||
"skills.plugin_management_host_only": "Plugin management requires the desktop app.",
|
||||
"skills.plugins_host_only": "Plugins are only available in the desktop app.",
|
||||
"skills.pick_project_for_plugins": "Pick a project folder to manage project plugins.",
|
||||
"skills.pick_project_for_active": "Pick a project folder to load active plugins.",
|
||||
"skills.no_opencode_found": "No opencode.json found yet. Add a plugin to create one.",
|
||||
@@ -250,13 +250,13 @@ export default {
|
||||
"skills.enter_plugin_name": "Enter a plugin package name.",
|
||||
"skills.plugin_already_listed": "Plugin already listed in opencode.json.",
|
||||
"skills.failed_update_opencode": "Failed to update opencode.json",
|
||||
"skills.opackage_install_host_only": "OpenPackage installs are only available in Host mode.",
|
||||
"skills.opackage_install_host_only": "OpenPackage installs require the desktop app.",
|
||||
"skills.pick_project_first": "Pick a project folder first.",
|
||||
"skills.enter_opackage_source": "Enter an OpenPackage source (e.g. github:anthropics/claude-code).",
|
||||
"skills.installing_opackage": "Installing OpenPackage...",
|
||||
"skills.install_complete": "Installed.",
|
||||
"skills.curated_list_notice": "This is a curated list, not an OpenPackage yet. Copy the link or watch the PRD for planned registry search integration.",
|
||||
"skills.import_host_only": "Skill import is only available in Host mode.",
|
||||
"skills.import_host_only": "Skill import requires the desktop app.",
|
||||
"skills.select_skill_folder": "Select skill folder",
|
||||
"skills.import_failed": "Import failed ({status})",
|
||||
"skills.imported": "Imported.",
|
||||
@@ -373,7 +373,7 @@ export default {
|
||||
"mcp.status_disabled": "Disabled",
|
||||
"mcp.disconnected": "Disconnected",
|
||||
"mcp.failed": "Failed",
|
||||
"mcp.host_mode_only": "MCP connections are only available in Host mode.",
|
||||
"mcp.host_mode_only": "MCP connections require the desktop app.",
|
||||
"mcp.pick_workspace_first": "Pick a workspace folder first.",
|
||||
"mcp.desktop_required": "MCP connections require the desktop app.",
|
||||
"mcp.connect_server_first": "Connect to the OpenCode server first.",
|
||||
@@ -422,7 +422,7 @@ export default {
|
||||
"mcp.auth.reauth_running": "Reauthorizing...",
|
||||
"mcp.auth.reauth_failed": "Reauthorization failed.",
|
||||
"mcp.auth.reauth_cli_hint": "Run: opencode mcp auth {server}",
|
||||
"mcp.auth.reauth_remote_hint": "Reauthorize from the host machine where this workspace runs.",
|
||||
"mcp.auth.reauth_remote_hint": "Reauthorize from the machine running this workspace.",
|
||||
"mcp.auth.authorization_still_required": "Authorization is still required. Try again to restart the flow.",
|
||||
"mcp.auth.oauth_not_supported_hint": "This could mean:\n• The MCP server doesn't advertise OAuth capabilities\n• The engine needs to reload to discover server capabilities\n• Try: opencode mcp auth {server} from the CLI",
|
||||
"mcp.auth.try_reload_engine": "{message}. Try reloading the engine first.",
|
||||
@@ -581,13 +581,13 @@ export default {
|
||||
"reload.toast_reload_stopped": "Reload & Stop Tasks",
|
||||
"reload.toast_reloading": "Reloading...",
|
||||
"reload.toast_dismiss": "Later",
|
||||
"reload.toast_blocked_host": "Reloading is only available in Host mode.",
|
||||
"reload.toast_blocked_host": "Reloading is only available for local workspaces.",
|
||||
"reload.toast_blocked_connect": "Connect to this workspace to reload.",
|
||||
"reload.toast_blocked_runs": "Waiting for active tasks to complete before reloading.",
|
||||
|
||||
// ==================== Onboarding ====================
|
||||
"onboarding.starting_host": "Starting OpenWork...",
|
||||
"onboarding.searching_host": "Searching for Host...",
|
||||
"onboarding.starting_host": "Starting OpenWork server...",
|
||||
"onboarding.searching_host": "Connecting to OpenWork server...",
|
||||
"onboarding.getting_ready": "Getting everything ready",
|
||||
"onboarding.verifying": "Verifying secure handshake",
|
||||
"onboarding.create_first_workspace": "Create your first workspace",
|
||||
@@ -605,10 +605,10 @@ export default {
|
||||
"onboarding.cli_ready": "OpenCode CLI ready.",
|
||||
"onboarding.cli_version": "OpenCode {version}",
|
||||
"onboarding.windows_install_instruction": "Install OpenCode for Windows, then restart OpenWork. Ensure opencode.exe is on PATH.",
|
||||
"onboarding.install_instruction": "Install OpenCode to enable host mode (no terminal required).",
|
||||
"onboarding.install_instruction": "Install OpenCode to enable the local server (no terminal required).",
|
||||
"onboarding.install": "Install OpenCode",
|
||||
"onboarding.recheck": "Re-check",
|
||||
"onboarding.ready_message": "OpenCode is ready to start in host mode.",
|
||||
"onboarding.ready_message": "OpenCode is ready to start the local server.",
|
||||
"onboarding.resolved_path": "Resolved path",
|
||||
"onboarding.version": "Version",
|
||||
"onboarding.search_notes": "Search notes",
|
||||
@@ -630,7 +630,7 @@ export default {
|
||||
"onboarding.remove": "Remove",
|
||||
"onboarding.cli_label": "OpenCode CLI",
|
||||
"onboarding.cli_checking": "Checking install...",
|
||||
"onboarding.cli_not_found_hint": "Not found. Install to run Host mode.",
|
||||
"onboarding.cli_not_found_hint": "Not found. Install to run the local server.",
|
||||
"onboarding.cli_version_installed": "Installed",
|
||||
"onboarding.cli_recheck": "Re-check",
|
||||
"onboarding.cli_install_commands": "Install OpenCode with one of the commands below, then restart OpenWork.",
|
||||
@@ -638,23 +638,23 @@ export default {
|
||||
"onboarding.last_checked": "Last checked {time}",
|
||||
"onboarding.server_url_placeholder": "http://localhost:8088",
|
||||
"onboarding.directory_placeholder": "my-project",
|
||||
"onboarding.connect_host": "Connect to Host",
|
||||
"onboarding.connect_host": "Connect to server",
|
||||
"onboarding.connect_description": "Pair with an existing OpenCode server (LAN or tunnel).",
|
||||
"onboarding.server_url": "Server URL",
|
||||
"onboarding.directory": "Directory (optional)",
|
||||
"onboarding.directory_hint": "Use if your host runs multiple workspaces.",
|
||||
"onboarding.directory_hint": "Use if your server runs multiple workspaces.",
|
||||
"onboarding.connect": "Connect",
|
||||
"onboarding.remote_workspace_title": "Connect OpenCode (recommended)",
|
||||
"onboarding.remote_workspace_description": "OpenWork connects directly by default. Use an OpenWork host to pair devices.",
|
||||
"onboarding.remote_workspace_title": "Connect to OpenWork server",
|
||||
"onboarding.remote_workspace_description": "Connect to an OpenWork server to access a workspace from anywhere.",
|
||||
"onboarding.remote_workspace_action": "Connect",
|
||||
"onboarding.remote_workspace_card_title": "Connect a remote workspace",
|
||||
"onboarding.remote_workspace_card_description": "Connect to OpenCode directly or pair with an OpenWork host.",
|
||||
"onboarding.advanced_openwork_host": "OpenWork host (alpha)",
|
||||
"onboarding.advanced_openwork_hint": "Use a host URL and client token for shared access.",
|
||||
"onboarding.remote_workspace_card_description": "Connect to an OpenWork server to access a shared workspace.",
|
||||
"onboarding.advanced_openwork_host": "OpenWork server",
|
||||
"onboarding.advanced_openwork_hint": "Use a server URL and access token for shared access.",
|
||||
"onboarding.advanced_opencode_direct": "Advanced: OpenCode direct",
|
||||
"onboarding.advanced_opencode_hint": "Connect straight to an OpenCode engine when no host is available.",
|
||||
"onboarding.advanced_opencode_hint": "Connect straight to an OpenCode engine when no server is available.",
|
||||
"onboarding.welcome_title": "How would you like to run OpenWork today?",
|
||||
"onboarding.run_local": "Run on this computer",
|
||||
"onboarding.run_local": "Run locally",
|
||||
"onboarding.run_local_description": "OpenWork runs OpenCode locally and keeps your work private.",
|
||||
"onboarding.engine_running": "Engine already running",
|
||||
"onboarding.attach_description": "Attach to the existing session on this device.",
|
||||
|
||||
@@ -51,21 +51,21 @@ export default {
|
||||
"dashboard.create_workspace_subtitle": "初始化新的基于文件夹的工作区。",
|
||||
"dashboard.create_workspace_confirm": "创建工作区",
|
||||
"dashboard.create_remote_workspace_title": "添加远程工作区",
|
||||
"dashboard.create_remote_workspace_subtitle": "将 OpenWork 主机或 OpenCode 引擎保存为工作区。",
|
||||
"dashboard.create_remote_workspace_subtitle": "保存 OpenWork 服务器为工作区。",
|
||||
"dashboard.create_remote_workspace_confirm": "添加工作区",
|
||||
"dashboard.remote_workspace_title": "远程工作区",
|
||||
"dashboard.remote_workspace_hint": "跟踪 OpenWork 主机或 OpenCode 引擎,随时重新连接。",
|
||||
"dashboard.remote_base_url_label": "OpenCode 地址",
|
||||
"dashboard.remote_base_url_placeholder": "http://127.0.0.1:4096",
|
||||
"dashboard.remote_workspace_hint": "记录 OpenWork 服务器,随时重新连接。",
|
||||
"dashboard.remote_base_url_label": "OpenWork 服务器地址",
|
||||
"dashboard.remote_base_url_placeholder": "http://127.0.0.1:8787",
|
||||
"dashboard.remote_base_url_required": "请先填写服务器地址。",
|
||||
"dashboard.openwork_host_label": "OpenWork 主机地址",
|
||||
"dashboard.openwork_host_placeholder": "https://your-host.openwork.app",
|
||||
"dashboard.openwork_host_hint": "使用 OpenWork 主机提供的地址。",
|
||||
"dashboard.openwork_host_token_label": "客户端令牌",
|
||||
"dashboard.openwork_host_label": "OpenWork 服务器地址",
|
||||
"dashboard.openwork_host_placeholder": "https://your-server.openwork.app",
|
||||
"dashboard.openwork_host_hint": "使用 OpenWork 服务器提供的地址。",
|
||||
"dashboard.openwork_host_token_label": "访问令牌",
|
||||
"dashboard.openwork_host_token_placeholder": "粘贴你的令牌",
|
||||
"dashboard.openwork_host_token_hint": "可选。粘贴主机提供的客户端令牌以完成配对。",
|
||||
"dashboard.remote_mode_openwork_alpha": "OpenWork 主机(Alpha)",
|
||||
"dashboard.remote_mode_direct": "OpenCode 直连(推荐)",
|
||||
"dashboard.openwork_host_token_hint": "可选。粘贴服务器提供的访问令牌以验证。",
|
||||
"dashboard.remote_mode_openwork_alpha": "OpenWork 服务器",
|
||||
"dashboard.remote_mode_direct": "直连(旧版)",
|
||||
"dashboard.remote_connection_openwork": "OpenWork",
|
||||
"dashboard.remote_connection_direct": "直连",
|
||||
"dashboard.remote_directory_label": "工作区目录(可选)",
|
||||
@@ -198,7 +198,7 @@ export default {
|
||||
"skills.add_title": "添加 skills",
|
||||
"skills.add_description": "安装起始命令、导入 skill,或打开文件夹。",
|
||||
"skills.install_from_openpackage": "从 OpenPackage 安装",
|
||||
"skills.host_mode_only": "仅主机模式",
|
||||
"skills.host_mode_only": "仅本地工作区",
|
||||
"skills.install": "安装",
|
||||
"skills.installed_label": "已安装",
|
||||
"skills.install_hint": "将 OpenPackage 包安装到当前工作区。Skills 应放在 `.opencode/skills` 中。",
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
"skills.installed": "已安装的 skills",
|
||||
"skills.no_skills": "在 `.opencode/skills` 或 `.claude/skills` 中未检测到 skills。",
|
||||
"skills.desktop_required": "技能管理需要桌面应用。",
|
||||
"skills.host_only_error": "技能管理仅在主机模式下可用。",
|
||||
"skills.host_only_error": "技能管理需要本地工作区或已连接的 OpenWork 服务器。",
|
||||
"skills.install_skill_creator": "安装 skill creator",
|
||||
"skills.install_skill_creator_hint": "此 skill 可让你在聊天中创建其他 skills。",
|
||||
"skills.installing_skill_creator": "正在安装 skill creator...",
|
||||
@@ -234,13 +234,13 @@ export default {
|
||||
"skills.notion_crm_title": "Notion CRM 增强技能",
|
||||
"skills.notion_crm_description": "为联系人、管道和跟进添加增强工作流。",
|
||||
"skills.notion_crm_card_description": "使用现成的技能丰富 Notion CRM 数据。",
|
||||
"skills.connect_host_to_load": "连接到主机以加载 skills。",
|
||||
"skills.connect_host_to_load": "连接 OpenWork 服务器以加载技能。",
|
||||
"skills.pick_workspace_first": "先选择一个工作区文件夹。",
|
||||
"skills.no_skills_found": "还没有找到 skills。",
|
||||
"skills.installed_description": "此工作区可用的 skills。",
|
||||
"skills.failed_to_load": "加载 skills 失败",
|
||||
"skills.plugin_management_host_only": "插件管理仅在主机模式下可用。",
|
||||
"skills.plugins_host_only": "插件仅在主机模式下可用。",
|
||||
"skills.plugin_management_host_only": "插件管理需要桌面应用。",
|
||||
"skills.plugins_host_only": "插件仅在桌面应用中可用。",
|
||||
"skills.pick_project_for_plugins": "选择项目文件夹以管理项目插件。",
|
||||
"skills.pick_project_for_active": "选择项目文件夹以加载活动插件。",
|
||||
"skills.no_opencode_found": "尚未找到 opencode.json。添加插件以创建一个。",
|
||||
@@ -251,13 +251,13 @@ export default {
|
||||
"skills.enter_plugin_name": "输入插件包名称。",
|
||||
"skills.plugin_already_listed": "插件已在 opencode.json 中列出。",
|
||||
"skills.failed_update_opencode": "更新 opencode.json 失败",
|
||||
"skills.opackage_install_host_only": "OpenPackage 安装仅在主机模式下可用。",
|
||||
"skills.opackage_install_host_only": "OpenPackage 安装需要桌面应用。",
|
||||
"skills.pick_project_first": "先选择一个项目文件夹。",
|
||||
"skills.enter_opackage_source": "输入 OpenPackage 源(例如:github:anthropics/claude-code)。",
|
||||
"skills.installing_opackage": "正在安装 OpenPackage...",
|
||||
"skills.install_complete": "已安装。",
|
||||
"skills.curated_list_notice": "这是一个精选列表,还不是 OpenPackage。复制链接或关注 PRD 以了解计划的注册表搜索集成。",
|
||||
"skills.import_host_only": "Skill 导入仅在主机模式下可用。",
|
||||
"skills.import_host_only": "Skill 导入需要桌面应用。",
|
||||
"skills.select_skill_folder": "选择 skill 文件夹",
|
||||
"skills.import_failed": "导入失败({status})",
|
||||
"skills.imported": "已导入。",
|
||||
@@ -378,7 +378,7 @@ export default {
|
||||
"mcp.status_disabled": "已禁用",
|
||||
"mcp.disconnected": "已断开",
|
||||
"mcp.failed": "失败",
|
||||
"mcp.host_mode_only": "MCP 连接仅在主机模式下可用。",
|
||||
"mcp.host_mode_only": "MCP 连接需要桌面应用。",
|
||||
"mcp.pick_workspace_first": "先选择一个工作区文件夹。",
|
||||
"mcp.desktop_required": "MCP 连接需要桌面应用。",
|
||||
"mcp.connect_server_first": "先连接到 OpenCode 服务器。",
|
||||
@@ -416,7 +416,7 @@ export default {
|
||||
"mcp.auth.reauth_running": "正在重新授权...",
|
||||
"mcp.auth.reauth_failed": "重新授权失败。",
|
||||
"mcp.auth.reauth_cli_hint": "运行:opencode mcp auth {server}",
|
||||
"mcp.auth.reauth_remote_hint": "请在运行该工作区的主机上重新授权。",
|
||||
"mcp.auth.reauth_remote_hint": "请在运行该工作区的设备上重新授权。",
|
||||
"mcp.auth.authorization_still_required": "仍需要授权。请重试以重新启动流程。",
|
||||
"mcp.auth.oauth_not_supported_hint": "这可能意味着:\n• MCP 服务器未声明 OAuth 功能\n• 引擎需要重新加载以发现服务器功能\n• 尝试:从 CLI 运行 opencode mcp auth {server}",
|
||||
"mcp.auth.try_reload_engine": "{message}。请尝试先重新加载引擎。",
|
||||
@@ -574,13 +574,13 @@ export default {
|
||||
"reload.toast_reload": "重新加载",
|
||||
"reload.toast_reloading": "正在重新加载...",
|
||||
"reload.toast_dismiss": "忽略",
|
||||
"reload.toast_blocked_host": "仅主机模式可重新加载。",
|
||||
"reload.toast_blocked_host": "仅本地工作区可重新加载。",
|
||||
"reload.toast_blocked_connect": "连接到此工作区后才能重新加载。",
|
||||
"reload.toast_blocked_runs": "请先停止活动运行再重新加载。",
|
||||
|
||||
// ==================== Onboarding ====================
|
||||
"onboarding.starting_host": "正在启动 OpenWork...",
|
||||
"onboarding.searching_host": "正在搜索主机...",
|
||||
"onboarding.starting_host": "正在启动 OpenWork 服务器...",
|
||||
"onboarding.searching_host": "正在连接 OpenWork 服务器...",
|
||||
"onboarding.getting_ready": "准备就绪",
|
||||
"onboarding.verifying": "验证安全握手",
|
||||
"onboarding.create_first_workspace": "创建您的第一个工作区",
|
||||
@@ -598,10 +598,10 @@ export default {
|
||||
"onboarding.cli_ready": "OpenCode CLI 就绪。",
|
||||
"onboarding.cli_version": "OpenCode {version}",
|
||||
"onboarding.windows_install_instruction": "安装 Windows 版 OpenCode,然后重启 OpenWork。确保 opencode.exe 在 PATH 中。",
|
||||
"onboarding.install_instruction": "安装 OpenCode 以启用主机模式(无需终端)。",
|
||||
"onboarding.install_instruction": "安装 OpenCode 以启用本地服务器(无需终端)。",
|
||||
"onboarding.install": "安装 OpenCode",
|
||||
"onboarding.recheck": "重新检查",
|
||||
"onboarding.ready_message": "OpenCode 已准备好以主机模式启动。",
|
||||
"onboarding.ready_message": "OpenCode 已准备好启动本地服务器。",
|
||||
"onboarding.resolved_path": "解析路径",
|
||||
"onboarding.version": "版本",
|
||||
"onboarding.search_notes": "搜索说明",
|
||||
@@ -623,7 +623,7 @@ export default {
|
||||
"onboarding.remove": "移除",
|
||||
"onboarding.cli_label": "OpenCode CLI",
|
||||
"onboarding.cli_checking": "正在检查安装...",
|
||||
"onboarding.cli_not_found_hint": "未找到。请安装以运行主机模式。",
|
||||
"onboarding.cli_not_found_hint": "未找到。请安装以运行本地服务器。",
|
||||
"onboarding.cli_version_installed": "已安装",
|
||||
"onboarding.cli_recheck": "重新检查",
|
||||
"onboarding.cli_install_commands": "使用以下命令之一安装 OpenCode,然后重启 OpenWork。",
|
||||
@@ -631,23 +631,23 @@ export default {
|
||||
"onboarding.last_checked": "上次检查时间 {time}",
|
||||
"onboarding.server_url_placeholder": "http://localhost:8088",
|
||||
"onboarding.directory_placeholder": "my-project",
|
||||
"onboarding.connect_host": "连接到主机",
|
||||
"onboarding.connect_host": "连接服务器",
|
||||
"onboarding.connect_description": "与现有的 OpenCode 服务器配对(局域网或隧道)。",
|
||||
"onboarding.server_url": "服务器 URL",
|
||||
"onboarding.directory": "目录(可选)",
|
||||
"onboarding.directory_hint": "如果主机运行多个工作区,请使用此选项。",
|
||||
"onboarding.directory_hint": "如果服务器运行多个工作区,请使用此选项。",
|
||||
"onboarding.connect": "连接",
|
||||
"onboarding.remote_workspace_title": "连接 OpenCode(推荐)",
|
||||
"onboarding.remote_workspace_description": "OpenWork 默认直接连接。需要跨设备时使用 OpenWork 主机。",
|
||||
"onboarding.remote_workspace_title": "连接 OpenWork 服务器",
|
||||
"onboarding.remote_workspace_description": "连接 OpenWork 服务器以随时访问工作区。",
|
||||
"onboarding.remote_workspace_action": "连接",
|
||||
"onboarding.remote_workspace_card_title": "连接远程工作区",
|
||||
"onboarding.remote_workspace_card_description": "直接连接 OpenCode,或配对 OpenWork 主机。",
|
||||
"onboarding.advanced_openwork_host": "OpenWork 主机(Alpha)",
|
||||
"onboarding.advanced_openwork_hint": "使用主机地址和客户端令牌进行共享访问。",
|
||||
"onboarding.remote_workspace_card_description": "连接 OpenWork 服务器以访问共享工作区。",
|
||||
"onboarding.advanced_openwork_host": "OpenWork 服务器",
|
||||
"onboarding.advanced_openwork_hint": "使用服务器地址和访问令牌进行共享访问。",
|
||||
"onboarding.advanced_opencode_direct": "高级:OpenCode 直连",
|
||||
"onboarding.advanced_opencode_hint": "当没有主机时,直接连接 OpenCode 引擎。",
|
||||
"onboarding.advanced_opencode_hint": "当没有服务器时,直接连接 OpenCode 引擎。",
|
||||
"onboarding.welcome_title": "今天想如何运行 OpenWork?",
|
||||
"onboarding.run_local": "在此计算机上运行",
|
||||
"onboarding.run_local": "本地运行",
|
||||
"onboarding.run_local_description": "OpenWork 在本地运行 OpenCode 并保持您的工作私密。",
|
||||
"onboarding.engine_running": "引擎已在运行",
|
||||
"onboarding.attach_description": "附加到此设备上的现有会话。",
|
||||
|
||||
@@ -58,6 +58,19 @@ export function startServer(config: ServerConfig) {
|
||||
return withCors(new Response(null, { status: 204 }), request, config);
|
||||
}
|
||||
|
||||
if (url.pathname === "/opencode" || url.pathname.startsWith("/opencode/")) {
|
||||
try {
|
||||
requireClient(request, config);
|
||||
const response = await proxyOpencodeRequest({ request, url, config });
|
||||
return withCors(response, request, config);
|
||||
} catch (error) {
|
||||
const apiError = error instanceof ApiError
|
||||
? error
|
||||
: new ApiError(500, "internal_error", "Unexpected server error");
|
||||
return withCors(jsonResponse(formatError(apiError), apiError.status), request, config);
|
||||
}
|
||||
}
|
||||
|
||||
const route = matchRoute(routes, request.method, url.pathname);
|
||||
if (!route) {
|
||||
return withCors(jsonResponse({ code: "not_found", message: "Not found" }, 404), request, config);
|
||||
@@ -119,6 +132,47 @@ function pathToRegex(path: string, keys: string[]): RegExp {
|
||||
return new RegExp(`^${pattern}$`);
|
||||
}
|
||||
|
||||
function buildOpencodeProxyUrl(baseUrl: string, path: string, search: string) {
|
||||
const target = new URL(baseUrl);
|
||||
const trimmedPath = path.replace(/^\/opencode/, "");
|
||||
target.pathname = trimmedPath.startsWith("/") ? trimmedPath : `/${trimmedPath}`;
|
||||
target.search = search;
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
async function proxyOpencodeRequest(input: {
|
||||
request: Request;
|
||||
url: URL;
|
||||
config: ServerConfig;
|
||||
}) {
|
||||
const workspace = input.config.workspaces[0];
|
||||
const baseUrl = workspace?.baseUrl?.trim() ?? "";
|
||||
if (!baseUrl) {
|
||||
throw new ApiError(400, "opencode_unconfigured", "OpenCode base URL is missing for this workspace");
|
||||
}
|
||||
|
||||
const targetUrl = buildOpencodeProxyUrl(baseUrl, input.url.pathname, input.url.search);
|
||||
const headers = new Headers(input.request.headers);
|
||||
headers.delete("authorization");
|
||||
headers.delete("host");
|
||||
headers.delete("origin");
|
||||
|
||||
const auth = workspace ? buildOpencodeAuthHeader(workspace) : null;
|
||||
if (auth) {
|
||||
headers.set("Authorization", auth);
|
||||
}
|
||||
|
||||
const method = input.request.method.toUpperCase();
|
||||
const body = method === "GET" || method === "HEAD" ? undefined : input.request.body;
|
||||
const response = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function jsonResponse(data: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
@@ -149,7 +203,10 @@ function withCors(response: Response, request: Request, config: ServerConfig) {
|
||||
if (!allowOrigin) return response;
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
||||
headers.set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-OpenWork-Host-Token, X-OpenWork-Client-Id");
|
||||
headers.set(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Authorization, Content-Type, X-OpenWork-Host-Token, X-OpenWork-Client-Id, X-OpenCode-Directory, X-Opencode-Directory",
|
||||
);
|
||||
headers.set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS");
|
||||
headers.set("Vary", "Origin");
|
||||
return new Response(response.body, { status: response.status, headers });
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -1315,8 +1315,8 @@ packages:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
get-tsconfig@4.13.0:
|
||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||
get-tsconfig@4.13.1:
|
||||
resolution: {integrity: sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
@@ -2676,7 +2676,7 @@ snapshots:
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-tsconfig@4.13.0:
|
||||
get-tsconfig@4.13.1:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
optional: true
|
||||
@@ -3064,7 +3064,7 @@ snapshots:
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
get-tsconfig: 4.13.0
|
||||
get-tsconfig: 4.13.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
optional: true
|
||||
|
||||
Reference in New Issue
Block a user