Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,22 @@ key_refused`; against an older backend the previous behaviour stands.
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))
- **Skill Hub works behind a corporate proxy** — browsing and installing from
the desktop app used to go straight to the network, ignoring the system
proxy and PAC settings that the app's own sign-in already honored, so on a
proxied network the Skill Hub page showed "Couldn't reach the Skill Hub"
while sign-in worked. Hub traffic, including the silent session refresh
behind it, now goes through the same proxy-aware network stack, and the app
waits for the proxy configuration to finish loading before the first hub
request instead of counting that wait against the request's own timeout.
- **Installing into a project on another machine is refused, not misplaced**
— when the app is connected to a WSL distro, a sandbox, or a remote server,
the install dialog no longer offers "This project": the app installs onto
the computer it runs on, so that folder is not here, and the previous
behavior created a stray folder on the wrong disk (or failed with an
unhelpful error). The dialog says which server you are connected to and
points at `codev skill pull` on that machine; the desktop side also checks
that a project folder really exists before it downloads anything.

### Language

Expand Down
2 changes: 1 addition & 1 deletion DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ from the squash message first, or use a plain merge.
| `packages/app/src/components/prompt-input-v2.tsx` | `onUnpaidClick` opens `DialogConnectProvider` (lazy import) instead of the free-models "unpaid" dialogs; their now-unused imports dropped. (The legacy `prompt-input.tsx` carried the same swap until its deletion — see the legacy prompt-input removal row.) | With the hosted providers hidden, "unpaid" means nothing is connected, and the free-models dialogs would render empty lists (this path is reachable on the web deployment; on desktop the gate covers it). | Keep the handler swap; take upstream's surrounding prompt-input changes. |
| `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 `<Show>`; 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. |
| 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, passing a `net.fetch` wrapper as the hub transport (`setHubTransport`) and a `warmProxy` hook — `session.defaultSession.resolveProxy(SKILLHUB_URL)` — so hub traffic honors enterprise proxies/PAC like the gateway SSO calls do and Chromium's proxy initialization is waited out before the first hub request; 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 `<Show>`; 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. |
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,8 @@ export const dict = {
"skillhub.install.scope.label": "Install to",
"skillhub.install.scope.project": "This project",
"skillhub.install.scope.global": "Global (all projects)",
"skillhub.install.scope.remote":
"You are connected to {{server}}. The app installs skills on this computer, so a project install is unavailable and a global install will not be seen there. Run `codev skill pull` on that machine instead.",
"skillhub.install.agents.label": "For which agents?",
"skillhub.install.agents.locked": "Always included",
"skillhub.install.overwrite.title": "Already installed",
Expand All @@ -1093,6 +1095,7 @@ export const dict = {
"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.error.missingDirectory": "That project folder is not on this computer.",
"skillhub.install.success.title": "Installed {{name}}",
"skillhub.install.done": "Done",
"skillhub.desktopOnly.title": "Skill Hub is available in the desktop app",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,8 @@ export const dict = {
"skillhub.install.scope.label": "Instalar en",
"skillhub.install.scope.project": "Este proyecto",
"skillhub.install.scope.global": "Global (todos los proyectos)",
"skillhub.install.scope.remote":
"Estás conectado a {{server}}. La aplicación instala las habilidades en este equipo, así que la instalación en el proyecto no está disponible y una instalación global no se verá allí. Ejecuta `codev skill pull` en esa máquina.",
"skillhub.install.agents.label": "¿Para qué agentes?",
"skillhub.install.agents.locked": "Siempre incluido",
"skillhub.install.overwrite.title": "Ya instalado",
Expand All @@ -1087,6 +1089,7 @@ export const dict = {
"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.error.missingDirectory": "Esa carpeta de proyecto no está en este equipo.",
"skillhub.install.success.title": "{{name}} instalado",
"skillhub.install.done": "Hecho",
"skillhub.desktopOnly.title": "Skill Hub está disponible en la aplicación de escritorio",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@ export const dict = {
"skillhub.install.scope.label": "Cài vào",
"skillhub.install.scope.project": "Dự án này",
"skillhub.install.scope.global": "Toàn cục (mọi dự án)",
"skillhub.install.scope.remote":
"Bạn đang kết nối tới {{server}}. Ứng dụng cài kỹ năng lên máy tính này, nên không thể cài vào dự án và cài toàn cục sẽ không hiện ở đó. Hãy chạy `codev skill pull` trên máy đó.",
"skillhub.install.agents.label": "Cho những agent nào?",
"skillhub.install.agents.locked": "Luôn được bao gồm",
"skillhub.install.overwrite.title": "Đã được cài đặt",
Expand All @@ -1050,6 +1052,7 @@ export const dict = {
"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.error.missingDirectory": "Thư mục dự án đó không có trên máy tính này.",
"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",
Expand Down
25 changes: 21 additions & 4 deletions packages/app/src/skillhub/dialog-install.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { displayName } from "@/pages/layout/helpers"
import { showToast } from "@/utils/toast"
import type { SkillHubAgent, SkillHubInstallOutcome, SkillHubScope } from "./types"
Expand Down Expand Up @@ -46,14 +47,20 @@ export function DialogSkillInstall(props: { id: string; name: string; version?:
const language = useLanguage()
const layout = useLayout()
const platform = usePlatform()
const server = useServer()

const projects = createMemo(() => layout.projects.list())
const preselected = createMemo(
() => projects().find((project) => project.worktree === layout.home.selection().directory) ?? projects()[0],
)
// The main process installs onto this machine's disk, but the project list
// belongs to the connected server. A WSL distro, sandbox, or remote host has
// worktrees that are not here, so "This project" only makes sense against
// the local server; global still works but that server will not see it.
const local = createMemo(() => server.isLocal())

const [state, setState] = createStore({
scope: (preselected() ? "project" : "global") as "project" | "global",
scope: (preselected() && local() ? "project" : "global") as "project" | "global",
directory: preselected()?.worktree as string | undefined,
agents: rememberedAgents(),
phase: "form" as "form" | "installing" | "signing-in" | "overwrite" | "done",
Expand Down Expand Up @@ -105,7 +112,12 @@ export function DialogSkillInstall(props: { id: string; name: string; version?:
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"
const key =
result.error.code === "network"
? "skillhub.error.network"
: result.error.code === "missing-directory"
? "skillhub.install.error.missingDirectory"
: "skillhub.install.error.failed"
setState({ phase: "form", error: `${language.t(key)} (${result.error.message})` })
}

Expand Down Expand Up @@ -154,10 +166,10 @@ export function DialogSkillInstall(props: { id: string; name: string; version?:
>
<RadioItemV2
value="project"
disabled={projects().length === 0}
disabled={projects().length === 0 || !local()}
label={language.t("skillhub.install.scope.project")}
/>
<Show when={state.scope === "project" && projects().length > 0}>
<Show when={state.scope === "project" && projects().length > 0 && local()}>
<SelectV2
appearance="inline"
class="ml-6"
Expand All @@ -174,6 +186,11 @@ export function DialogSkillInstall(props: { id: string; name: string; version?:
</Show>
<RadioItemV2 value="global" label={language.t("skillhub.install.scope.global")} />
</RadioGroupV2>
<Show when={!local()}>
<div class="text-[12px] text-v2-text-text-muted">
{language.t("skillhub.install.scope.remote", { server: server.name })}
</div>
</Show>
</div>

<div class="flex flex-col gap-2">
Expand Down
7 changes: 4 additions & 3 deletions packages/app/src/skillhub/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ export type SkillHubInstallOutcome = {

// Discriminated results instead of throws: ipcMain.handle rejections arrive as
// mangled "Error invoking remote method" strings, while `already-installed`
// must stay machine-readable to drive the overwrite offer and `network` the
// hub-unreachable state.
// must stay machine-readable to drive the overwrite offer, `network` the
// hub-unreachable state, and `missing-directory` the case where the chosen
// project folder is not on the machine the app runs on.
export type SkillHubError = {
code: "auth" | "not-found" | "already-installed" | "network" | "unknown"
code: "auth" | "not-found" | "already-installed" | "network" | "missing-directory" | "unknown"
message: string
}

Expand Down
17 changes: 15 additions & 2 deletions packages/codev-skillhub/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,30 @@ import { applySystemCaCertsOnce, isCertError } from "./tls"
// failed.
async function hubFetch(endpoint: string, input: string | URL, init?: RequestInit): Promise<Response> {
try {
return await fetch(input, init)
return await httpApi.transport(input, init)
} catch (err) {
if (!isCertError(err)) throw err
const ca = applySystemCaCertsOnce()
if (ca?.status !== "merged") throw err
return await fetch(input, init)
return await httpApi.transport(input, init)
}
}

// Object indirection so tests can stub HTTP without module mocks (Bun spyOn on
// ESM namespaces is unreliable).
export const httpApi = {
fetch: hubFetch,
// The socket-level transport behind every hub request, including the silent
// SSO refresh that authenticates them. Node's fetch ignores system proxies
// and PAC files, so a host with a proxy-aware stack swaps its own in (see
// setHubTransport); the CLI keeps the default.
transport: fetch as typeof fetch,
}

// Route all hub traffic through `fetchImpl`. The desktop app's Electron main
// process passes Chromium's net.fetch so enterprise proxies, PAC scripts, and
// OS trust roots apply to Skill Hub browsing and installs the same way they
// already do to the gateway SSO calls.
export function setHubTransport(fetchImpl: typeof fetch) {
httpApi.transport = fetchImpl
}
2 changes: 2 additions & 0 deletions packages/codev-skillhub/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { defaultAgents } from "./agents"
export { pathExists } from "./archive"
export { SKILLHUB_URL } from "./const"
export { setHubTransport } from "./http"
export { describeNetworkError } from "./tls"
export { stripControlChars } from "./sanitize"
export { AGENT_LABELS, ALWAYS_AGENT, SKILL_AGENTS, isSkillAgent, type Scope, type SkillAgent } from "./skill-dirs"
Expand Down
4 changes: 2 additions & 2 deletions packages/codev-skillhub/src/skillhub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function skillhubAuthHeaders(optional: boolean): Promise<Record<string, st
const cookie = loadSkillhubCookie()
if (cookie) return { Cookie: cookie }

const auth = await silentSso()
const auth = await silentSso({ fetchImpl: httpApi.transport })
if (auth) return { Authorization: `Bearer ${auth.access_token}` }

if (optional) return {}
Expand All @@ -46,7 +46,7 @@ async function skillhubAuthHeaders(optional: boolean): Promise<Record<string, st
export async function hasSkillhubAuth(): Promise<boolean> {
if (loadSkillhubCookie()) return true
try {
return (await silentSso()) !== null
return (await silentSso({ fetchImpl: httpApi.transport })) !== null
} catch {
return false
}
Expand Down
Loading
Loading