From 2925ef07242a12ae70daf7bec566b6211f59b879 Mon Sep 17 00:00:00 2001 From: quickbeard Date: Wed, 26 Aug 2026 15:23:47 -0700 Subject: [PATCH 1/5] fix(app): unwrap store proxy before Skill Hub install IPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install dialog passed state.agents — a SolidJS store's reactive Proxy — into ipcRenderer.invoke, whose structured clone rejects proxies with "An object could not be cloned". The rejection happened at the IPC layer, below the controller's result-union error handling, so nothing reset the dialog and it hung on "Installing" forever. Copy the agents into a plain array at the call site. The browse and detail calls already pass plain objects and strings. Co-Authored-By: Claude Fable 5 --- packages/app/src/skillhub/dialog-install.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app/src/skillhub/dialog-install.tsx b/packages/app/src/skillhub/dialog-install.tsx index a19e2fad8..bfcc0740c 100644 --- a/packages/app/src/skillhub/dialog-install.tsx +++ b/packages/app/src/skillhub/dialog-install.tsx @@ -83,7 +83,9 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: } catch { // Remember-last-choice only; a blocked localStorage changes nothing. } - const result = await skillhub.install({ id: props.id, scope, agents: state.agents, force }) + // Copy the agents out of the store: ipcRenderer.invoke structured-clones + // its arguments, and a store's reactive Proxy is not cloneable. + const result = await skillhub.install({ id: props.id, scope, agents: [...state.agents], force }) if (result.ok) { setState({ phase: "done", outcome: result.value }) showToast(language.t("skillhub.install.success.title", { name: props.name })) From c4f31824d335345e4473df12982a67d5359e59a7 Mon Sep 17 00:00:00 2001 From: quickbeard Date: Wed, 26 Aug 2026 15:26:19 -0700 Subject: [PATCH 2/5] fix(tui): declare the @codev/upload workspace dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/app.tsx and src/component/dialog-configure.tsx import @codev/upload/nudge and /spawn (since #312) but the package never declared the dependency, so bun never linked it into packages/tui/node_modules and `bun typecheck` failed with TS2307 on any machine where hoisting didn't happen to cover it — which also blocked every push through the pre-push hook's repo-wide typecheck. Co-Authored-By: Claude Fable 5 --- bun.lock | 1 + packages/tui/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/bun.lock b/bun.lock index 2655aff46..6f983ede6 100644 --- a/bun.lock +++ b/bun.lock @@ -1005,6 +1005,7 @@ "dependencies": { "@codev/brand": "workspace:*", "@codev/gateway": "workspace:*", + "@codev/upload": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/tui/package.json b/packages/tui/package.json index 25e428498..8d33163ad 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -50,6 +50,7 @@ "dependencies": { "@codev/brand": "workspace:*", "@codev/gateway": "workspace:*", + "@codev/upload": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/sdk": "workspace:*", From aaa1eccd0f5469963663911957d458a33697654b Mon Sep 17 00:00:00 2001 From: quickbeard Date: Wed, 26 Aug 2026 15:43:58 -0700 Subject: [PATCH 3/5] fix(app): give Skill Hub install failures their own error messages Install failures reused the browse page's "Something went wrong loading the Skill Hub" copy, and an expired-session failure gave no hint that signing in again is the remedy. Map the auth code to a re-login hint and other failures to install-specific copy (en/es/vi). Co-Authored-By: Claude Fable 5 --- packages/app/src/i18n/en.ts | 3 +++ packages/app/src/i18n/es.ts | 3 +++ packages/app/src/i18n/vi.ts | 3 +++ packages/app/src/skillhub/dialog-install.tsx | 7 ++++++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 36f7aac74..601ca8a36 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1076,6 +1076,9 @@ export const dict = { "skillhub.install.cancel": "Cancel", "skillhub.install.confirm": "Install", "skillhub.install.installing": "Installing…", + "skillhub.install.error.auth": + "Your SkillHub session has expired. Sign in again (run `codevhub login` in a terminal), then retry.", + "skillhub.install.error.failed": "The Skill Hub could not deliver this skill.", "skillhub.install.success.title": "Installed {{name}}", "skillhub.install.done": "Done", "skillhub.desktopOnly.title": "Skill Hub is available in the desktop app", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index f5f418dd8..b9f06ce4c 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -1069,6 +1069,9 @@ export const dict = { "skillhub.install.cancel": "Cancelar", "skillhub.install.confirm": "Instalar", "skillhub.install.installing": "Instalando…", + "skillhub.install.error.auth": + "Tu sesión de SkillHub ha caducado. Inicia sesión de nuevo (ejecuta `codevhub login` en una terminal) y vuelve a intentarlo.", + "skillhub.install.error.failed": "El Skill Hub no pudo entregar esta habilidad.", "skillhub.install.success.title": "{{name}} instalado", "skillhub.install.done": "Hecho", "skillhub.desktopOnly.title": "Skill Hub está disponible en la aplicación de escritorio", diff --git a/packages/app/src/i18n/vi.ts b/packages/app/src/i18n/vi.ts index 208afc02b..c5375e7f7 100644 --- a/packages/app/src/i18n/vi.ts +++ b/packages/app/src/i18n/vi.ts @@ -1034,6 +1034,9 @@ export const dict = { "skillhub.install.cancel": "Hủy", "skillhub.install.confirm": "Cài đặt", "skillhub.install.installing": "Đang cài đặt…", + "skillhub.install.error.auth": + "Phiên đăng nhập SkillHub đã hết hạn. Hãy đăng nhập lại (chạy `codevhub login` trong terminal) rồi thử lại.", + "skillhub.install.error.failed": "Skill Hub không thể tải kỹ năng này về.", "skillhub.install.success.title": "Đã cài đặt {{name}}", "skillhub.install.done": "Xong", "skillhub.desktopOnly.title": "Skill Hub có sẵn trong ứng dụng desktop", diff --git a/packages/app/src/skillhub/dialog-install.tsx b/packages/app/src/skillhub/dialog-install.tsx index bfcc0740c..f69168d52 100644 --- a/packages/app/src/skillhub/dialog-install.tsx +++ b/packages/app/src/skillhub/dialog-install.tsx @@ -96,7 +96,12 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: setState({ phase: "overwrite", existingPath: match?.[1] ?? result.error.message }) return } - const key = result.error.code === "network" ? "skillhub.error.network" : "skillhub.error.unknown" + const key = + result.error.code === "network" + ? "skillhub.error.network" + : result.error.code === "auth" + ? "skillhub.install.error.auth" + : "skillhub.install.error.failed" setState({ phase: "form", error: `${language.t(key)} (${result.error.message})` }) } From ba0bd3b2da9729a9fb26fb15bf9f46b869347085 Mon Sep 17 00:00:00 2001 From: quickbeard Date: Wed, 26 Aug 2026 15:52:39 -0700 Subject: [PATCH 4/5] feat(app): offer in-app SkillHub sign-in from the install dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An expired or missing SkillHub session surfaced as an error telling the user to run `codevhub login` in a terminal, even though the desktop app already owns a browser SSO flow (the gateway configure gate) that writes the same shared auth file every hub request reads. When an install fails with the auth code, the dialog now shows a Sign in button that runs `gatewaySso.startLogin()` — silent refresh first, full browser loopback otherwise — then retries the install automatically. A signing-in phase shows a loader with a cancel that aborts the attempt. The auth error copy drops the terminal hint in all three locales. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 6 +- packages/app/src/i18n/en.ts | 5 +- packages/app/src/i18n/es.ts | 5 +- packages/app/src/i18n/vi.ts | 5 +- packages/app/src/skillhub/dialog-install.tsx | 71 ++++++++++++++++---- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e3830f7..ee72a5534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,10 @@ file is the human-readable summary of what changed and why. globally with the same agent picker the CLI uses — CoDev Code always included, one real copy plus links for the other agents. Re-installing offers a safe overwrite, and everything rides the same shared sign-in as - `codev skill` and `codevhub`. - ([#331](https://github.com/quickbeard/codev-code/pull/331)) + `codev skill` and `codevhub` — when the hub asks for a signed-in session, + the install dialog offers the browser sign-in right in the app. + ([#331](https://github.com/quickbeard/codev-code/pull/331), + [#335](https://github.com/quickbeard/codev-code/pull/335)) ### Language diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 601ca8a36..c0607cfaa 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1076,8 +1076,9 @@ export const dict = { "skillhub.install.cancel": "Cancel", "skillhub.install.confirm": "Install", "skillhub.install.installing": "Installing…", - "skillhub.install.error.auth": - "Your SkillHub session has expired. Sign in again (run `codevhub login` in a terminal), then retry.", + "skillhub.install.error.auth": "Your SkillHub session has expired. Sign in to continue.", + "skillhub.install.signin": "Sign in", + "skillhub.install.signin.waiting": "Finish signing in in your browser…", "skillhub.install.error.failed": "The Skill Hub could not deliver this skill.", "skillhub.install.success.title": "Installed {{name}}", "skillhub.install.done": "Done", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index b9f06ce4c..f0338be90 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -1069,8 +1069,9 @@ export const dict = { "skillhub.install.cancel": "Cancelar", "skillhub.install.confirm": "Instalar", "skillhub.install.installing": "Instalando…", - "skillhub.install.error.auth": - "Tu sesión de SkillHub ha caducado. Inicia sesión de nuevo (ejecuta `codevhub login` en una terminal) y vuelve a intentarlo.", + "skillhub.install.error.auth": "Tu sesión de SkillHub ha caducado. Inicia sesión para continuar.", + "skillhub.install.signin": "Iniciar sesión", + "skillhub.install.signin.waiting": "Termina de iniciar sesión en tu navegador…", "skillhub.install.error.failed": "El Skill Hub no pudo entregar esta habilidad.", "skillhub.install.success.title": "{{name}} instalado", "skillhub.install.done": "Hecho", diff --git a/packages/app/src/i18n/vi.ts b/packages/app/src/i18n/vi.ts index c5375e7f7..eb2d5bdb1 100644 --- a/packages/app/src/i18n/vi.ts +++ b/packages/app/src/i18n/vi.ts @@ -1034,8 +1034,9 @@ export const dict = { "skillhub.install.cancel": "Hủy", "skillhub.install.confirm": "Cài đặt", "skillhub.install.installing": "Đang cài đặt…", - "skillhub.install.error.auth": - "Phiên đăng nhập SkillHub đã hết hạn. Hãy đăng nhập lại (chạy `codevhub login` trong terminal) rồi thử lại.", + "skillhub.install.error.auth": "Phiên đăng nhập SkillHub đã hết hạn. Hãy đăng nhập để tiếp tục.", + "skillhub.install.signin": "Đăng nhập", + "skillhub.install.signin.waiting": "Hoàn tất đăng nhập trong trình duyệt…", "skillhub.install.error.failed": "Skill Hub không thể tải kỹ năng này về.", "skillhub.install.success.title": "Đã cài đặt {{name}}", "skillhub.install.done": "Xong", diff --git a/packages/app/src/skillhub/dialog-install.tsx b/packages/app/src/skillhub/dialog-install.tsx index f69168d52..de405cd13 100644 --- a/packages/app/src/skillhub/dialog-install.tsx +++ b/packages/app/src/skillhub/dialog-install.tsx @@ -1,5 +1,6 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { CheckboxV2 } from "@opencode-ai/ui/v2/checkbox-v2" +import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2" import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2" import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" import { RadioGroupV2, RadioItemV2 } from "@opencode-ai/ui/v2/radio-v2" @@ -55,7 +56,8 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: scope: (preselected() ? "project" : "global") as "project" | "global", directory: preselected()?.worktree as string | undefined, agents: rememberedAgents(), - phase: "form" as "form" | "installing" | "overwrite" | "done", + phase: "form" as "form" | "installing" | "signing-in" | "overwrite" | "done", + needsAuth: false, error: undefined as string | undefined, existingPath: undefined as string | undefined, outcome: undefined as SkillHubInstallOutcome | undefined, @@ -77,7 +79,7 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: state.scope === "project" && state.directory ? { kind: "project", directory: state.directory } : { kind: "global" } - setState({ phase: "installing", error: undefined }) + setState({ phase: "installing", needsAuth: false, error: undefined }) try { localStorage.setItem(AGENT_CHOICE_KEY, JSON.stringify(state.agents)) } catch { @@ -96,15 +98,41 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: setState({ phase: "overwrite", existingPath: match?.[1] ?? result.error.message }) return } - const key = - result.error.code === "network" - ? "skillhub.error.network" - : result.error.code === "auth" - ? "skillhub.install.error.auth" - : "skillhub.install.error.failed" + if (result.error.code === "auth") { + // The hub wants a signed-in session — the dialog can run the same SSO + // flow the configure gate uses, so offer that instead of the raw + // message's run-a-CLI hint. + setState({ phase: "form", needsAuth: true, error: language.t("skillhub.install.error.auth") }) + return + } + const key = result.error.code === "network" ? "skillhub.error.network" : "skillhub.install.error.failed" setState({ phase: "form", error: `${language.t(key)} (${result.error.message})` }) } + const signIn = async () => { + const sso = platform.gatewaySso + if (!sso) return + setState({ phase: "signing-in", error: undefined }) + const failure = await sso.startLogin().then( + () => undefined, + (error: unknown) => (error instanceof Error ? error.message : String(error)), + ) + // A cancelled login is the user pressing Cancel below, not a failure — + // cancelSignIn already put the form back. + if (failure?.includes("cancelled")) return + if (failure !== undefined) { + setState({ phase: "form", error: failure }) + return + } + setState("needsAuth", false) + await install(false) + } + + const cancelSignIn = async () => { + await platform.gatewaySso?.cancelLogin().catch(() => {}) + setState({ phase: "form" }) + } + const [selectedProject, setSelectedProject] = createSignal(preselected()) return ( @@ -170,6 +198,13 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: + +
+ +
{language.t("skillhub.install.signin.waiting")}
+
+
+
@@ -204,10 +239,22 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: dialog.close()}> {language.t("skillhub.install.cancel")} - void install(false)}> - {state.phase === "installing" - ? language.t("skillhub.install.installing") - : language.t("skillhub.install.confirm")} + void install(false)}> + {state.phase === "installing" + ? language.t("skillhub.install.installing") + : language.t("skillhub.install.confirm")} + + } + > + void signIn()}>{language.t("skillhub.install.signin")} + + + + void cancelSignIn()}> + {language.t("skillhub.install.cancel")} From 026d9faa7fd42720ae5491277866f23695440db1 Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Thu, 27 Aug 2026 08:12:39 +0700 Subject: [PATCH 5/5] fix(app): pre-bundle the markdown worker's deps in dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite's dep scanner does not crawl `?worker&url` entries, so the markdown worker's deps (@shikijs/stream, katex, marked, marked-shiki, remend, shiki) were only discovered on the first markdown render — a mid-session re-optimize that full-page reloads the dev app, resetting e.g. a Skill Hub search as its results rendered. Scan the worker as an extra optimizeDeps entry so they are bundled at startup instead. Co-Authored-By: Claude Fable 5 --- DIVERGENCES.md | 1 + packages/app/vite.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 8bd3b0b33..e90b055ef 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -224,6 +224,7 @@ from the squash message first, or use a plain merge. | `packages/desktop/src/renderer/index.tsx` | Calls `installConsoleFormatting()` (fork-only `./console-format`) before anything else logs. | `spyRendererConsole` receives Chromium's flattened console-message string, so any renderer `console.*` with an object argument reached the terminal and the exported debug logs as `[object Object]`; the wrapper serializes structured arguments (Error-aware, circular-safe, truncated) at the source. | Keep the import and call; take upstream's surrounding entry changes. | | Desktop gateway SSO wiring: `packages/desktop/src/main/index.ts`, `packages/desktop/src/preload/{index,types}.ts`, `packages/desktop/src/renderer/index.tsx` | Main instantiates the fork-only `src/main/gateway-sso/` controller (wrapping `@codev/gateway` `login`/`silentSso` — node-only loopback OAuth — with `shell.openExternal` and a `net.fetch` wrapper) and registers its `gateway-sso-*` IPC channels next to the sandbox ones, passing an `onConfigured` callback that restarts the running sandboxes (`SandboxController.restartRunning`) because a guest is handed its codev config once at boot and cannot see credentials that appear afterwards; preload exposes the `gatewaySso` namespace (subscribe fan-out, sandbox pattern) with types re-exported from `@opencode-ai/app/gateway/types`; the renderer platform maps `gatewaySso: window.api.gatewaySso`. | SSO login for the desktop configure gate must run in the main process (`node:http` loopback server, `~/.codev/auth.json` writes); `net.fetch` so enterprise proxies/PAC apply (egress precedent). | Keep the wiring; take upstream's surrounding changes. Keep channel names `gateway-sso-*` stable. | | Desktop Skill Hub wiring: `packages/desktop/src/main/index.ts`, `packages/desktop/src/preload/{index,types}.ts`, `packages/desktop/src/renderer/index.tsx`, `packages/desktop/package.json`, `packages/app/src/app.tsx`, `packages/app/src/i18n/{en,es}.ts` | Main registers the fork-only `src/main/skillhub/` controller (over the fork-only `@codev/skillhub` package) and its `skillhub-*` IPC channels; preload exposes the `skillhub` namespace (types aliased from `@opencode-ai/app/skillhub/types`); renderer maps `skillhub: window.api.skillhub` into the platform; `@codev/skillhub` added under desktop devDependencies (electron-vite bundles devDependency workspace packages); app.tsx registers lazy `/skills` + `/skills/:id` routes inside the new-layout ``; i18n adds `sidebar.skillhub` + the `skillhub.*` family (vi.ts is fork-only). | The Skill Hub page is native (fork-only `packages/app/src/{skillhub/**,pages/skill-hub*.tsx,components/skill-hub-nav.tsx}`); all hub HTTP must originate in the Electron main process because the hub serves no ACAO on /api/v1 and 403s cross-origin writes, so the renderer reaches it only over IPC. | Keep the registration/preload/renderer/package.json lines, the two route lines, and the i18n keys; take upstream's surrounding changes. | +| `packages/app/vite.js` | Added `optimizeDeps.entries`: the default `index.html` plus session-ui's `markdown.worker.ts` (absolute path derived from the plugin file, forward-slashed for fast-glob), so the dev-time dep scanner crawls the markdown worker at startup. | Vite's dep scanner does not crawl `?worker&url` entries, so the worker's deps (`@shikijs/stream`, `katex`, `marked`, `marked-shiki`, `remend`, `shiki`) were first discovered when the first markdown rendered; that mid-session re-optimize full-page reloads the dev app — e.g. a Skill Hub search reset itself the moment results rendered. A bare `optimizeDeps.include` list cannot express this fix: with bun's isolated linker the deps live only in the workspace packages' own `node_modules`, and no single include chain resolves from both consumers' roots (`packages/app` and the desktop renderer's `src/renderer`). Dev-only; production builds ignore `optimizeDeps`. | Keep the `optimizeDeps` block and keep `index.html` listed in it — setting `entries` replaces Vite's default entry inference, so dropping it unscans the app itself. If upstream moves or renames the worker file, update the path: a stale pattern matches nothing, logs nothing, and silently brings the reload back. | | `packages/app/package.json` | Added `"./gateway/types": "./src/gateway/types.ts"`, `"./skillhub/types": "./src/skillhub/types.ts"`, and `"./i18n/*": "./src/i18n/*.ts"` to `exports`. | The desktop preload aliases the app-owned gateway SSO wire types (sandbox/types pattern); the desktop main process imports the locale dicts to translate the macOS menu bar (`./i18n/*` mirrors how `@opencode-ai/ui` already exports its own — see the localized desktop menu row). | Keep both exports; take upstream's other manifest changes. | | `packages/app/src/i18n/{en,es}.ts` | Added the `gateway.configure.*` key family (configure-gate dialog) and reworded `sidebar.gettingStarted.line1` — the "includes free models" pitch no longer holds with the hosted providers hidden. | Strings for the fork's configure gate; the getting-started card must not advertise models the fork does not offer. | Keep our keys and copy; take upstream's surrounding i18n changes. | | `packages/desktop/src/main/index.ts` | Deleted upstream's `app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")`. | Chromium's `<-loopback>` bypass rule means the opposite of what it reads as: it *subtracts* the implicit rules that keep loopback off the proxy, i.e. it opts localhost traffic *into* proxying (`net/docs/proxy.md`). Today it is inert — Electron reads `--proxy-bypass-list` only together with `--proxy-server` (`shell/browser/browser_process_impl.cc`), which the app never passes — but it becomes actively harmful the moment anything launches the app with `--proxy-server`: the renderer's sidecar traffic would be routed into the proxy. Chromium already bypasses loopback by default, so nothing replaces it. | Keep deleted. If upstream adds new proxy switches, check them against Chromium's implicit-bypass semantics before taking them. | diff --git a/packages/app/vite.js b/packages/app/vite.js index b0fbfed34..f23236e7c 100644 --- a/packages/app/vite.js +++ b/packages/app/vite.js @@ -5,6 +5,13 @@ import { fileURLToPath } from "url" const theme = fileURLToPath(new URL("./public/oc-theme-preload.js", import.meta.url)) +// Absolute so it resolves from any consumer's root (the desktop renderer's is +// src/renderer); forward slashes because optimizeDeps.entries is a fast-glob +// pattern, which rejects backslashes. +const markdownWorker = fileURLToPath( + new URL("../session-ui/src/components/markdown.worker.ts", import.meta.url), +).replaceAll("\\", "/") + const channel = (() => { const raw = process.env.CODEV_CHANNEL if (raw === "dev" || raw === "beta" || raw === "prod") return raw @@ -31,6 +38,16 @@ export default [ worker: { format: "es", }, + optimizeDeps: { + // Vite's startup dep scanner does not crawl `?worker&url` entries, + // so the markdown worker's deps (@shikijs/stream, katex, marked, + // marked-shiki, remend) were only discovered on the first markdown + // render — a mid-session re-optimize that full-page reloads the app + // in dev (e.g. resetting a Skill Hub search). Scanning the worker as + // an extra entry pre-bundles them at startup. "index.html" restates + // the default entry, which setting `entries` would otherwise drop. + entries: ["index.html", markdownWorker], + }, } }, },