From b94812c0306ae28937c6287c210d2741570e5d9f Mon Sep 17 00:00:00 2001 From: Edison-A-N Date: Sun, 24 May 2026 17:04:17 +0800 Subject: [PATCH 1/4] fix(launcher): support custom workspace endpoints --- packages/launcher/src/main/agent-manager.ts | 120 +++++++++++++++++--- packages/launcher/src/main/index.ts | 7 +- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/packages/launcher/src/main/agent-manager.ts b/packages/launcher/src/main/agent-manager.ts index c18480ed9..efbf72c1b 100644 --- a/packages/launcher/src/main/agent-manager.ts +++ b/packages/launcher/src/main/agent-manager.ts @@ -28,6 +28,25 @@ const LAUNCHER_SESSIONS_DIR = path.join(CONFIG_DIR, "launcher-sessions") const DEFAULT_CHAT_CHANNEL = "main" const CHAT_POLL_INTERVAL_MS = 2500 +interface LauncherSettingsStore { + get(key?: string): unknown +} + +function normalizeWorkspaceEndpoint(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const raw = value.trim() + if (!raw) return undefined + try { + const url = new URL(raw) + if (url.hostname === "workspace.openagents.org") { + return url.origin.replace("workspace.openagents.org", "workspace-endpoint.openagents.org") + } + return url.origin + } catch { + return raw.replace(/\/$/, "") + } +} + export interface InstalledAgentRecord { name: string version: string | null @@ -258,9 +277,10 @@ function extractToolCalls(msg: ChatMessage): ChatToolCall[] | undefined { export function extractMentions(text: string): string[] { const out: string[] = [] const re = /(^|\s)@([a-zA-Z0-9_-]+)/g - let m - while ((m = re.exec(text)) !== null) { - if (!out.includes(m[2])) out.push(m[2]) + let match = re.exec(text) + while (match !== null) { + if (!out.includes(match[2])) out.push(match[2]) + match = re.exec(text) } return out } @@ -374,7 +394,7 @@ function resolveWorkingNode( let core: Record | null = loadCore() export class AgentManager extends EventEmitter { - private _store: unknown + private _store: LauncherSettingsStore private _healthByType = new Map() private _healthRefreshInFlight = new Set() private _lastHealthRefreshAt = 0 @@ -409,18 +429,32 @@ export class AgentManager extends EventEmitter { private _chatPolls = new Map() _connector: Record | null = null - constructor(store: unknown) { + constructor(store: LauncherSettingsStore) { super() this._store = store if (!core) core = loadCore() if (core) { - const AgentConnector = (core as Record) - .AgentConnector as new (opts: unknown) => Record - this._connector = new AgentConnector({ configDir: CONFIG_DIR }) + this._connector = this.createConnector() } ensureDir(LAUNCHER_SESSIONS_DIR) } + private createConnector(): Record { + const AgentConnector = (core as Record) + .AgentConnector as new (opts: unknown) => Record + const workspaceEndpoint = normalizeWorkspaceEndpoint( + this._store.get("workspaceEndpoint"), + ) + return new AgentConnector({ + configDir: CONFIG_DIR, + ...(workspaceEndpoint ? { workspaceEndpoint } : {}), + }) + } + + private configuredWorkspaceEndpoint(): string | undefined { + return normalizeWorkspaceEndpoint(this._store.get("workspaceEndpoint")) + } + getSupportedAgentTypes(): string[] { const supported = (core as Record | null)?.adapters ? Object.keys( @@ -451,9 +485,7 @@ export class AgentManager extends EventEmitter { for (const k of cacheKeys) delete require.cache[k] core = loadCore() if (core) { - const AgentConnector = (core as Record) - .AgentConnector as new (opts: unknown) => Record - this._connector = new AgentConnector({ configDir: CONFIG_DIR }) + this._connector = this.createConnector() } this.clearCatalogCache() this._agentsCache = { value: [], at: 0 } @@ -806,6 +838,24 @@ export class AgentManager extends EventEmitter { }) } + private parseCustomWorkspaceUrl( + urlStr: string, + ): { endpoint?: string; slug?: string; token?: string } | null { + try { + const u = new URL(urlStr.trim()) + const host = u.hostname.toLowerCase() + if (host === "workspace.openagents.org") { + return null + } + const endpoint = u.origin + const slug = u.pathname.replace(/^\//, "").split("/")[0] || undefined + const token = u.searchParams.get("token") || undefined + return { endpoint, slug, token } + } catch { + return null + } + } + async registerWorkspaceFromToken(input: { url?: string token?: string @@ -820,6 +870,38 @@ export class AgentManager extends EventEmitter { const tokenOrSlug = (input.token || input.slug || input.url || "").trim() if (!tokenOrSlug) throw new Error("Missing workspace URL or token") + const customParsed = input.url ? this.parseCustomWorkspaceUrl(input.url) : null + if (customParsed) { + const slug = input.slug || customParsed.slug + const token = input.token || customParsed.token + if (!slug) + throw new Error( + "Custom workspace URL must include slug (first path segment) or provide slug explicitly", + ) + if (!token) + throw new Error( + "Custom workspace URL must include token query parameter or provide token explicitly", + ) + + const config = this._connector!.config as Record + const addNetwork = config.addNetwork as (opts: unknown) => unknown + addNetwork.call(config, { + id: slug, + slug, + name: slug, + endpoint: customParsed.endpoint, + token, + }) + this.signalReload() + return { + id: slug, + slug, + name: slug, + endpoint: customParsed.endpoint, + token, + } + } + const resolveToken = this._connector!.resolveToken as ( token: string, ) => Promise<{ @@ -831,6 +913,7 @@ export class AgentManager extends EventEmitter { const info = await resolveToken.call(this._connector, tokenOrSlug) const slug = info.slug || info.workspace_id || input.slug if (!slug) throw new Error("Could not resolve workspace from input") + const endpoint = info.endpoint || this.configuredWorkspaceEndpoint() const config = this._connector!.config as Record const addNetwork = config.addNetwork as (opts: unknown) => unknown @@ -838,7 +921,7 @@ export class AgentManager extends EventEmitter { id: info.workspace_id, slug, name: info.name || slug, - endpoint: info.endpoint, + endpoint, token: input.token || tokenOrSlug, }) this.signalReload() @@ -846,7 +929,7 @@ export class AgentManager extends EventEmitter { id: info.workspace_id, slug, name: info.name || slug, - endpoint: info.endpoint, + endpoint, token: input.token || tokenOrSlug, } } @@ -867,6 +950,7 @@ export class AgentManager extends EventEmitter { const info = await resolveToken.call(this._connector, tokenOrSlug) const slug = info.slug || info.workspace_id const wsName = info.name || slug + const endpoint = info.endpoint || this.configuredWorkspaceEndpoint() const addNetwork = (this._connector!.config as Record) .addNetwork as (opts: unknown) => void @@ -874,7 +958,7 @@ export class AgentManager extends EventEmitter { id: info.workspace_id, slug, name: wsName, - endpoint: info.endpoint, + endpoint, token: tokenOrSlug, }) @@ -883,7 +967,13 @@ export class AgentManager extends EventEmitter { slug: string, ) => void connectWorkspace.call(this._connector, agentName, slug as string) - } catch { + } catch (err) { + const networks = this.getNetworks() as Array<{ id?: string; slug?: string }> + const existing = networks.some( + (network) => network.slug === tokenOrSlug || network.id === tokenOrSlug, + ) + if (!existing) throw err + const connectWorkspace = this._connector!.connectWorkspace as ( name: string, slug: string, diff --git a/packages/launcher/src/main/index.ts b/packages/launcher/src/main/index.ts index 7ad7a932b..0dbb22c92 100644 --- a/packages/launcher/src/main/index.ts +++ b/packages/launcher/src/main/index.ts @@ -1415,7 +1415,12 @@ function setupIPC(): void { ) ipcMain.handle("settings:get", (_e, key) => store.get(key)) - ipcMain.handle("settings:set", (_e, key, value) => store.set(key, value)) + ipcMain.handle("settings:set", (_e, key, value) => { + store.set(key, value) + if (key === "workspaceEndpoint" && agentManager) { + agentManager.reloadCore() + } + }) // ── Connections ── ipcMain.handle("connections:list", () => connectionsStore.list()) From f2ad6b88631a7f780a29866f3694e06bae7e42fb Mon Sep 17 00:00:00 2001 From: Edison-A-N Date: Sun, 24 May 2026 17:04:25 +0800 Subject: [PATCH 2/4] fix(launcher): open workspace links from network endpoints --- .../components/workspaces/WorkspaceCard.tsx | 3 +- .../workspaces/WorkspaceQuickConnect.tsx | 32 ++++++++++++------- .../src/renderer/lib/workspace-urls.ts | 15 +++++++++ .../src/renderer/pages/workspaces/index.tsx | 7 ++-- 4 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 packages/launcher/src/renderer/lib/workspace-urls.ts diff --git a/packages/launcher/src/renderer/components/workspaces/WorkspaceCard.tsx b/packages/launcher/src/renderer/components/workspaces/WorkspaceCard.tsx index bee97fb80..9e1392391 100644 --- a/packages/launcher/src/renderer/components/workspaces/WorkspaceCard.tsx +++ b/packages/launcher/src/renderer/components/workspaces/WorkspaceCard.tsx @@ -11,6 +11,7 @@ import { } from "./WorkspaceRecentActivity" import { platformLabel } from "../connections/platforms" import type { Agent, Workspace } from "../../types" +import { workspaceDisplayHost } from "../../lib/workspace-urls" export interface WorkspaceCardData { ws: Workspace @@ -72,7 +73,7 @@ export function WorkspaceCard({
- workspace.openagents.org/{slug} + {workspaceDisplayHost(ws.endpoint)}/{slug}
diff --git a/packages/launcher/src/renderer/components/workspaces/WorkspaceQuickConnect.tsx b/packages/launcher/src/renderer/components/workspaces/WorkspaceQuickConnect.tsx index a96541a1c..a4f5e8cee 100644 --- a/packages/launcher/src/renderer/components/workspaces/WorkspaceQuickConnect.tsx +++ b/packages/launcher/src/renderer/components/workspaces/WorkspaceQuickConnect.tsx @@ -57,31 +57,39 @@ export function WorkspaceQuickConnect({ } }, [open]) - const parseInput = (raw: string): { slug?: string; token?: string } => { + const parseInput = ( + raw: string, + ): { url?: string; slug?: string; token?: string; customUrl?: boolean } => { const v = raw.trim() if (!v) return {} try { const u = new URL(v) const slug = u.pathname.replace(/^\//, "").split("/")[0] || undefined const token = u.searchParams.get("token") || undefined - if (slug || token) return { slug, token } + return { + url: v, + slug, + token, + customUrl: u.hostname.toLowerCase() !== "workspace.openagents.org", + } } catch {} return { token: v } } const handlePasteConnect = async (): Promise => { - const { slug, token } = parseInput(pasted) - if (!slug && !token) { + const parsed = parseInput(pasted) + const { slug, token } = parsed + if (!parsed.url && !slug && !token) { showToast("Paste a workspace URL or token", "warning") return } setBusy(true) try { - const ws = await window.api.registerWorkspaceFromToken({ - url: pasted.trim() || undefined, - token, - slug, - }) + const ws = await window.api.registerWorkspaceFromToken( + parsed.customUrl + ? { url: parsed.url } + : { url: parsed.url, token, slug }, + ) const label = ws.name || ws.slug || slug || "workspace" showToast( `Registered ${label}. Open Agents tab to connect an agent.`, @@ -157,11 +165,11 @@ export function WorkspaceQuickConnect({ setPasted(e.target.value)} - placeholder="https://workspace.openagents.org/my-team?token=…" + placeholder="https://workspace.openagents.org/my-team?token=… or http://localhost:8000/my-team?token=…" autoFocus />
- Token and slug are extracted automatically. + Hosted URLs use remote token resolution. Custom URLs use the URL origin, first path segment, and token query parameter.
@@ -211,7 +219,7 @@ export function WorkspaceQuickConnect({ "Copy the workspace URL and paste it in the “Paste URL / token” tab.", ].map((step, i) => (
  • diff --git a/packages/launcher/src/renderer/lib/workspace-urls.ts b/packages/launcher/src/renderer/lib/workspace-urls.ts new file mode 100644 index 000000000..66d8c45fd --- /dev/null +++ b/packages/launcher/src/renderer/lib/workspace-urls.ts @@ -0,0 +1,15 @@ +const DEFAULT_WORKSPACE_WEB_BASE_URL = "https://workspace.openagents.org" + +export function workspaceWebBaseUrl(endpoint?: string): string { + const baseUrl = (endpoint || DEFAULT_WORKSPACE_WEB_BASE_URL).replace(/\/$/, "") + return baseUrl.replace("workspace-endpoint", "workspace").replace(/\/v1$/, "") +} + +export function workspaceDisplayHost(endpoint?: string): string { + const baseUrl = workspaceWebBaseUrl(endpoint) + try { + return new URL(baseUrl).host + } catch { + return baseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "") + } +} diff --git a/packages/launcher/src/renderer/pages/workspaces/index.tsx b/packages/launcher/src/renderer/pages/workspaces/index.tsx index 3d4717161..8e7d4426e 100644 --- a/packages/launcher/src/renderer/pages/workspaces/index.tsx +++ b/packages/launcher/src/renderer/pages/workspaces/index.tsx @@ -16,6 +16,7 @@ import { useWorkspacePrefs } from "../../store/workspace-prefs" import { useUiStore } from "../../store/ui" import type { Agent, ChatSessionMeta, Workspace } from "../../types" import type { ToastType } from "../../hooks/useToast" +import { workspaceWebBaseUrl } from "../../lib/workspace-urls" interface Props { showToast: (msg: string, type?: ToastType) => void @@ -217,7 +218,8 @@ export default function Workspaces({ showToast }: Props): React.JSX.Element { const handleCopyUrl = async (ws: Workspace): Promise => { markUsed(ws.id) const slug = ws.slug || ws.id - const url = `https://workspace.openagents.org/${slug}` + const baseUrl = workspaceWebBaseUrl(ws.endpoint) + const url = `${baseUrl}/${slug}` const full = ws.token ? `${url}?token=${encodeURIComponent(ws.token)}` : url try { await navigator.clipboard.writeText(full) @@ -232,7 +234,8 @@ export default function Workspaces({ showToast }: Props): React.JSX.Element { const handleOpenBrowser = (ws: Workspace): void => { markUsed(ws.id) const slug = ws.slug || ws.id - let url = `https://workspace.openagents.org/${slug}` + const baseUrl = workspaceWebBaseUrl(ws.endpoint) + let url = `${baseUrl}/${slug}` if (ws.token) url += `?token=${encodeURIComponent(ws.token)}` window.api.openExternal(url) } From 685ee0d684ed198e834e049d99a50fac94a99b3f Mon Sep 17 00:00:00 2001 From: Edison-A-N Date: Sun, 24 May 2026 17:04:41 +0800 Subject: [PATCH 3/4] fix(launcher): expose workspace endpoint settings --- .../src/renderer/pages/agents/index.tsx | 55 ++++++++++++------- .../src/renderer/pages/settings/index.tsx | 26 +++++++-- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/packages/launcher/src/renderer/pages/agents/index.tsx b/packages/launcher/src/renderer/pages/agents/index.tsx index 7a203bfe3..6a5c2e1d6 100644 --- a/packages/launcher/src/renderer/pages/agents/index.tsx +++ b/packages/launcher/src/renderer/pages/agents/index.tsx @@ -12,6 +12,7 @@ import { TopBar } from "../../components/TopBar" import type { Agent, CatalogEntry, EnvField, HealthCheck } from "../../types" import type { ToastType } from "../../hooks/useToast" import { cn } from "../../lib/utils" +import { workspaceWebBaseUrl } from "../../lib/workspace-urls" function formatHealthLabel(health: HealthCheck | null): string { if (!health) return "Not configured" @@ -164,7 +165,7 @@ export default function Agents({ showToast }: AgentsProps): React.JSX.Element { (w) => w.slug === agent.network || w.id === agent.network, ) const slug = (ws && ws.slug) || agent.network - let url = `https://workspace.openagents.org/${slug}` + let url = `${workspaceWebBaseUrl(ws?.endpoint)}/${slug}` if (ws && ws.token) url += `?token=${encodeURIComponent(ws.token)}` window.api.openExternal(url) } catch (err: unknown) { @@ -482,8 +483,9 @@ function NewAgentDialog({ ) : ( <>
    - +
    - + setAgentName(e.target.value)} @@ -504,8 +507,9 @@ function NewAgentDialog({ />
    - + setAgentPath(e.target.value)} @@ -681,12 +685,13 @@ function ConfigureDialog({
    {fields.map((f) => (
    -