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/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/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/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 36f7aac74..c0607cfaa 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1076,6 +1076,10 @@ 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 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", "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..f0338be90 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -1069,6 +1069,10 @@ 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 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", "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..eb2d5bdb1 100644 --- a/packages/app/src/i18n/vi.ts +++ b/packages/app/src/i18n/vi.ts @@ -1034,6 +1034,10 @@ 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 để 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", "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 a19e2fad8..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,13 +79,15 @@ 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 { // 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 })) @@ -94,10 +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" : "skillhub.error.unknown" + 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 ( @@ -163,6 +198,13 @@ export function DialogSkillInstall(props: { id: string; name: string; version?: + +
+ +
{language.t("skillhub.install.signin.waiting")}
+
+
+
@@ -197,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")} 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], + }, } }, }, 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:*",