Files
Edwin He 6bb128c804 feat(web): automatic dev sharding + bare workspace URL for npm run dev (#4713)
Point OMNIGENT_URL at a bare Databricks workspace origin and npm run dev
auto-fills the /api/2.0/omnigent api-proxy mount and emits the host_id slice
key on host-scoped traffic (build-time VITE_DATABRICKS_WORKSPACE flag + the
unified isDatabricksWorkspace() gate), so the standalone dev bundle shards like
the embedded UI. An explicit mount or a local server is unaffected.

Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
Co-authored-by: Isaac <isaac@example.com>
2026-08-13 04:46:06 +00:00

310 lines
12 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import type { Plugin, ProxyOptions } from "vite";
import { defineConfig } from "vitest/config";
// Databricks workspace-hosted omnigent is mounted behind the api-proxy at this
// path; a local / self-hosted server mounts at the root. Mirrors the Python
// WORKSPACE_API_PATH (omnigent/cli_auth.py) so both sides agree on the shape of
// a workspace URL.
const WORKSPACE_API_PATH = "/api/2.0/omnigent";
function isWorkspaceHost(hostname: string): boolean {
return hostname.endsWith(".databricks.com") || hostname.endsWith(".azuredatabricks.net");
}
// Resolve the proxy target. Point OMNIGENT_URL at a bare workspace origin
// (https://<ws>.databricks.com) and the api-proxy mount is filled in
// automatically, so the browser's relative /v1/... paths reach the
// workspace-hosted server without hand-typing the /api/2.0/omnigent prefix. An
// explicit non-root path is respected (a custom mount, or a local server).
function resolveTarget(raw: string): string {
const url = new URL(raw);
if (isWorkspaceHost(url.hostname) && (url.pathname === "" || url.pathname === "/")) {
url.pathname = WORKSPACE_API_PATH;
}
return url.toString().replace(/\/$/, "");
}
const OMNIGENT_URL = resolveTarget(process.env.OMNIGENT_URL ?? "http://localhost:6767");
let cachedToken: string | null | undefined;
function resolveToken(host: string): string | null {
if (cachedToken !== undefined) return cachedToken;
if (process.env.OMNIGENT_AUTH_TOKEN) {
cachedToken = process.env.OMNIGENT_AUTH_TOKEN;
return cachedToken;
}
try {
const output = execFileSync(
"databricks",
["auth", "token", "--host", host, "--output", "json"],
{
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
},
);
const tokenResponse = JSON.parse(output) as { access_token?: string };
cachedToken = tokenResponse.access_token ?? null;
} catch {
cachedToken = null;
}
return cachedToken;
}
function configureProxy(target: string, useAuth: boolean): NonNullable<ProxyOptions["configure"]> {
const parsed = new URL(target);
const host = parsed.origin;
// The URL pathname becomes a prefix prepended to every proxied request.
// e.g. OMNIGENT_URL=https://host.com/api/2.0/omnigent means the browser's
// /v1/sessions is rewritten to /api/2.0/omnigent/v1/sessions before forwarding.
const basePath = parsed.pathname.replace(/\/$/, "");
return (proxy) => {
proxy.on("proxyReq", (proxyReq) => {
if (basePath) proxyReq.path = `${basePath}${proxyReq.path}`;
if (useAuth) {
const token = resolveToken(host);
if (token) proxyReq.setHeader("Authorization", `Bearer ${token}`);
}
});
proxy.on("proxyReqWs", (proxyReq) => {
if (basePath) proxyReq.path = `${basePath}${proxyReq.path}`;
if (useAuth) {
const token = resolveToken(host);
if (token) proxyReq.setHeader("Authorization", `Bearer ${token}`);
}
});
proxy.on("proxyRes", (proxyRes, _req, res) => {
const contentType = proxyRes.headers["content-type"] ?? "";
if (typeof contentType === "string" && contentType.includes("text/event-stream")) {
// http-proxy applies upstream headers after its own proxyRes listener
// runs. Defer flushing until after those headers have been copied.
setImmediate(() => res.flushHeaders());
}
});
};
}
function createProxyConfig(target: string, useAuth: boolean): Record<string, ProxyOptions> {
const origin = new URL(target).origin;
const configure = configureProxy(target, useAuth);
return {
"/v1": {
target: origin,
changeOrigin: true,
ws: true,
configure,
},
"/api": {
target: origin,
changeOrigin: true,
configure,
},
"/auth": {
target: origin,
changeOrigin: true,
configure,
},
"/health": {
target: origin,
changeOrigin: true,
configure,
},
// The server's version manifest (/.well-known/omnigent.json), which the
// desktop shell reads to learn what it's talking to. Without this the dev
// server answers with the SPA's index.html, and the shell — which rightly
// refuses to parse HTML as a manifest — sees every dev server as
// "pre-manifest", making the capability invisible in local development.
"/.well-known": {
target: origin,
changeOrigin: true,
configure,
},
};
}
const parsed = new URL(OMNIGENT_URL);
const useAuth = !!process.env.OMNIGENT_AUTH_TOKEN || isWorkspaceHost(parsed.hostname);
// A Databricks workspace-hosted server sits behind the multi-replica sharding
// layer, so the dev bundle must emit the host_id slice key on host-scoped
// traffic — same as the embedded (managed) UI — or requests scatter across
// replicas and a host's runner tunnel becomes unreachable ("host is offline").
// Surface it to the browser as a build-time flag (inlined into import.meta.env);
// the embed path keys off its host fetcher instead (see isDatabricksWorkspace in
// host.ts). Keyed off the resolved mount so pointing at a bare workspace origin
// turns it on.
const isDatabricksWorkspaceEnv = parsed.pathname.replace(/\/$/, "") === WORKSPACE_API_PATH;
if (isDatabricksWorkspaceEnv) process.env.VITE_DATABRICKS_WORKSPACE = "true";
if (useAuth) {
const token = resolveToken(parsed.origin);
if (token) {
console.log(
`[dev-proxy] target=${OMNIGENT_URL} (authenticated${isDatabricksWorkspaceEnv ? ", databricks-workspace" : ""})`,
);
} else {
console.error(
`\n[dev-proxy] ERROR: No auth token for ${parsed.origin}.\n` +
` Set OMNIGENT_AUTH_TOKEN or run: databricks auth login --host ${parsed.origin}\n`,
);
process.exit(1);
}
} else {
console.log(`[dev-proxy] target=${OMNIGENT_URL}`);
}
const proxyConfig = createProxyConfig(OMNIGENT_URL, useAuth);
/**
* Emit the tombstone `sw.js` for the standalone build (see `sw-src/sw.js`): the
* retired PWA service worker's self-unregistering replacement. Registered ONLY
* here (not in `vite.embed.config.ts`), so the embed island still ships no
* service worker.
*
* @deprecated Delete this plugin together with `sw-src/sw.js` in 0.11.0, once
* existing registrations have had time to unregister themselves.
*/
function emitServiceWorkerTombstone(): Plugin {
return {
name: "emit-service-worker-tombstone",
generateBundle() {
this.emitFile({
type: "asset",
fileName: "sw.js",
source: readFileSync(path.resolve(__dirname, "sw-src/sw.js"), "utf8"),
});
},
};
}
// Safari < 16.4 cannot parse regex lookbehind; these dependency regexes would
// otherwise throw there, at module scope during boot or on the first rendered
// markdown message (#1978):
// - marked probes lookbehind support with `new RegExp("(?<=1)(?<!1)")` inside
// try/catch, but rolldown constant-folds the probe to `true`; route the
// constructor through `globalThis` so it stays a runtime check.
// - remend builds its single-tilde repair regex at module scope with no
// guard; fall back to a never-matching regex so the repair no-ops.
// - mdast-util-gfm-autolink-literal's email regex is constructed when a GFM
// message renders; fall back to a never-matching regex (plain-text emails
// just don't autolink).
const LOOKBEHIND_REWRITES: [string, string][] = [
['new RegExp("(?<=1)(?<!1)")', 'new globalThis.RegExp("(?<=1)(?<!1)")'],
[
'new RegExp("(?<=[\\\\p{L}\\\\p{N}_])~(?!~)(?=[\\\\p{L}\\\\p{N}_])","gu")',
'(() => { try { return new globalThis.RegExp("(?<=[\\\\p{L}\\\\p{N}_])~(?!~)(?=[\\\\p{L}\\\\p{N}_])", "gu"); } catch { return /(?!)/gu; } })()',
],
[
"/(?<=^|\\s|\\p{P}|\\p{S})([-.\\w+]+)@([-\\w]+(?:\\.[-\\w]+)+)/gu",
'(() => { try { return new globalThis.RegExp("(?<=^|\\\\s|\\\\p{P}|\\\\p{S})([-.\\\\w+]+)@([-\\\\w]+(?:\\\\.[-\\\\w]+)+)", "gu"); } catch { return /(?!)/gu; } })()',
],
];
const LOOKBEHIND_REWRITE_MODULES = [
"/node_modules/marked/",
"/node_modules/remend/",
"/node_modules/mdast-util-gfm-autolink-literal/",
];
function isLookbehindRewriteModule(id: string): boolean {
const normalizedId = id.replaceAll("\\", "/");
return LOOKBEHIND_REWRITE_MODULES.some((modulePath) => normalizedId.includes(modulePath));
}
function safariLookbehindWorkarounds(): Plugin {
return {
name: "safari-lookbehind-workarounds",
transform(code, id) {
if (!isLookbehindRewriteModule(id)) return;
let out = code;
for (const [from, to] of LOOKBEHIND_REWRITES) {
out = out.replaceAll(from, to);
}
if (out === code) return;
return { code: out, map: null };
},
};
}
export default defineConfig({
plugins: [emitServiceWorkerTombstone(), safariLookbehindWorkarounds(), react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test-setup.ts"],
// Scope discovery to src/ — the web suite lives there. Without this,
// vitest's default glob descends into the nested electron package and
// tries to run its node:test files (which aren't vitest suites).
include: ["src/**/*.{test,spec}.?(c|m)[jt]s?(x)"],
coverage: {
provider: "v8",
// With `include` set, vitest counts every matching source file (untested
// ones as 0%), so the total reflects the whole frontend — parity with the
// backend's --cov=omnigent, not just files a test happened to import.
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.test.{ts,tsx}",
"src/**/*.d.ts",
"src/test-setup.ts",
// Vendored UI kit, not product code (see tests/e2e_ui/COVERAGE_GAPS.md).
"src/components/ai-elements/**",
],
reportsDirectory: "./coverage",
// text-summary: human-readable console line; json-summary: machine-
// readable coverage/coverage-summary.json that CI distills to total.txt.
reporter: ["text-summary", "json-summary"],
},
},
server: {
proxy: proxyConfig,
},
build: {
// default baseline is Safari 16.4+; iPadOS 15 can't parse dep regex lookbehinds (#1978)
target: ["chrome111", "edge111", "firefox114", "safari15", "ios15"],
outDir: path.resolve(__dirname, "../omnigent/server/static/web-ui"),
emptyOutDir: true,
rollupOptions: {
output: {
manualChunks(id) {
const normalized = id.replaceAll("\\", "/");
// Shiki lazily imports each language grammar (`@shikijs/langs/<lang>`)
// via dynamic import; leave those as their own on-demand chunks
// instead of folding ~200 grammars into the eagerly-loaded core.
if (normalized.includes("/@shikijs/langs/")) {
return;
}
// Keep Shiki's core, engines, and bundle glue (incl. the language
// index + alias map) in one chunk. pnpm's symlinks + Vite's default
// split otherwise expose a top-level cyclic import between the
// language bundle and the alias-map chunk that executes before its
// data dependency is initialized, producing "Cannot read properties
// of undefined (reading 'flatMap')" and a blank Monaco/file-viewer
// screen. The engines must stay here too: excluding them splits the
// cyclic core across chunks and reintroduces the bug.
if (normalized.includes("/shiki") || normalized.includes("/@shikijs/")) {
return "shiki";
}
return undefined;
},
},
},
},
});