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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ file is the human-readable summary of what changed and why.

## Unreleased

### Skills

- **Skill Hub in the desktop app** — a new Skill Hub entry in the sidebar
(just above Settings) opens a native browser for the CoDev SkillHub:
search, sort, and filter the registry by domain, read a skill's SKILL.md
with stars and download counts, and install it into the current project or
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))

### Language

- **CoDev speaks Vietnamese** — the desktop and web app, the shared UI
Expand Down
5 changes: 3 additions & 2 deletions DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,12 @@ from the squash message first, or use a plain merge.
| `packages/app/src/context/global-sync/utils.ts` | `normalizeProviderList` returns through `filterHiddenProviders` (fork-only `src/gateway/hidden-providers.ts`): upstream's hosted providers `opencode` (Zen) and `opencode-go` are dropped from `all`/`default`, and the unauthenticated free-tier `opencode` from `connected` (a deliberately configured paid Zen key still counts as connected, mirroring the TUI's `isConnectedProvider`). | Client-side hiding at the one choke point both the global bootstrap and per-directory catalogs flow through — mirrors the TUI's dialog filters without writing `disabled_providers`. Downstream surfaces keyed off those catalog entries (`dialog-select-model-unpaid{,-v2}.tsx`, the `usage-exceeded-dialogs.tsx` Go upsell, Zen branches in `dialog-connect-provider.tsx`, hosted entries in `use-providers.ts` `popularProviders`, settings badges) become deliberately-dead code left byte-identical to upstream so merges stay cheap. | Keep the import + wrap; take upstream's normalization changes. Do not "clean up" the dead hosted-provider surfaces — they cost nothing and would churn every sync. |
| `packages/ui/src/context/dialog.tsx` | Per-dialog `persistent` flag (precedent: the TUI's persistent dialog, see the `packages/tui/src/ui/dialog.tsx` row): Escape, backdrop click, and Kobalte `onOpenChange` are no-ops for a dialog marked persistent; programmatic `close()` still works; `setPersistent` targets the top dialog so pushed sub-dialogs stay dismissable; `useDialog()` gains `persistent`/`setPersistent`. | The desktop configure gate needs a dialog the user cannot dismiss while no provider is configured. | Keep the flag and the three guards; take upstream's surrounding dialog changes. |
| `packages/app/src/app.tsx` | `<GatewayConfigureGate />` (fork-only `components/gateway/`) mounted as the first child inside `ServerScopedProviders`' `ModelsProvider`. | The desktop configure gate; the placement is load-bearing — completing the flow seeds the live `useModels()` recents store (`buildGatewayConfig` deliberately writes no `config.model` pin). | Keep the mount inside `ModelsProvider`; take upstream's surrounding shell changes. |
| `packages/app/src/context/platform.tsx` | Added optional `gatewaySso?: GatewaySsoPlatform` capability (wire types in fork-only `src/gateway/types.ts`, exported as `@opencode-ai/app/gateway/types` — see the `packages/app/package.json` row). | Desktop-only SSO login for the configure gate; absence of the capability is the web deployment's kill-switch. | Keep the field + import; take upstream's surrounding platform changes. |
| `packages/app/src/context/platform.tsx` | Added optional `gatewaySso?: GatewaySsoPlatform` and `skillhub?: SkillHubPlatform` capabilities (skillhub wire types in fork-only `src/skillhub/types.ts`) (wire types in fork-only `src/gateway/types.ts`, exported as `@opencode-ai/app/gateway/types` — see the `packages/app/package.json` row). | Desktop-only SSO login for the configure gate; absence of the capability is the web deployment's kill-switch. | Keep the field + import; take upstream's surrounding platform changes. |
| `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. |
| `packages/app/package.json` | Added `"./gateway/types": "./src/gateway/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. |
| 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. |
| `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. |
| Startup proxy-stall gate: `packages/desktop/src/main/{index,ipc}.ts`, `packages/desktop/src/preload/{index,types}.ts`, `packages/desktop/src/renderer/index.tsx`, `packages/app/src/app.tsx`, `packages/app/src/context/platform.tsx` | Three additive pieces. (1) `main/index.ts` fires `session.defaultSession.resolveProxy(url)` for the sidecar URL as soon as the port is known (fire-and-forget, result logged). (2) A `local-server-health` IPC chain: main answers with the Node-fetch `checkHealth` against the spawned sidecar once `serverReady` resolves (`false` if initialization failed), preload exposes `localServerHealth()`, the renderer platform maps it, and `platform.tsx` gains the optional `localServerHealth?()` capability. (3) `app.tsx` `ConnectionGate`: the health-check effect is restructured around an inner `poll`; when the 10 s blocking window expires without a healthy answer and the connection is the built-in sidecar, it asks `platform.localServerHealth` — if the host process vouches for the server it keeps blocking on the splash for up to another 50 s instead of showing the unreachable error. Background mode and `type: "http"` behavior are unchanged, and without the capability (web) the gate behaves exactly as upstream. | Chromium queues every renderer request — loopback included — until proxy initialization (PAC download / WPAD discovery) resolves; the implicit loopback-bypass rules are only applied once the resolution service is READY (`net/proxy_resolution/configured_proxy_resolution_service.cc`). On corporate networks that takes tens of seconds (measured 21 s vs 64 ms with an unreachable PAC host), so users behind a proxy saw "Could not reach Local Server" at every launch while the server was healthy the whole time — main's own Node-fetch health check (loopback in `NO_PROXY`) had already passed before the window opened. The prewarm overlaps proxy init with sidecar startup; the gate fallback distinguishes "server down" from "renderer network stack not ready". | Keep all three pieces; take upstream's surrounding changes. If upstream restructures `ConnectionGate`, re-apply the vouch-then-extend fallback around whatever its blocking window becomes. Keep the `local-server-health` channel name stable. |
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"./wsl/types": "./src/wsl/types.ts",
"./sandbox/types": "./src/sandbox/types.ts",
"./gateway/types": "./src/gateway/types.ts",
"./skillhub/types": "./src/skillhub/types.ts",
"./vite": "./vite.js",
"./index.css": "./src/index.css"
},
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ import { NewHome, LegacyHome } from "@/pages/home"

const NewSession = lazy(() => import("@/pages/new-session"))

const SkillHubPage = lazy(() => import("@/pages/skill-hub"))
const SkillHubDetailPage = lazy(() => import("@/pages/skill-hub-detail"))

const SessionRoute = () => {
const settings = useSettings()
const params = useParams()
Expand Down Expand Up @@ -671,6 +674,8 @@ function Routes(props: { serverScoped?: JSX.Element }) {
</Route>
<Show when={settings.general.newLayoutDesigns()}>
<Route path="/" component={NewHome} />
<Route path="/skills" component={SkillHubPage} />
<Route path="/skills/:id" component={SkillHubDetailPage} />
<Route path="/:dir/session/:id" component={NewLayoutLegacySessionRedirect} />
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
</Show>
Expand Down
22 changes: 15 additions & 7 deletions packages/app/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { usePlatform } from "@/context/platform"
import { ServerConnection } from "@/context/server"
import { sessionHasOpenTab, useTabs } from "@/context/tabs"
import { createHomeModel, isBackgroundOpen, type HomeSessionRecord } from "@/pages/home-model"
import { SkillHubNav } from "@/components/skill-hub-nav"
// The project rows, server rows and utility nav still live in home.tsx, where
// upstream keeps them — this file composes them into a persistent column and
// adds the Recents section. Only the `export` keywords there are ours.
Expand Down Expand Up @@ -258,13 +259,20 @@ export function AppSidebar(props: { class?: string; debugTools?: { visible: bool
{/* An expanded section is the flexible child that holds this down. When
every section is collapsed nothing grows, so the auto margin takes
over and the slack opens below the last section instead of pushing
it — and Settings/Help — to the bottom of the column. */}
<HomeUtilityNav
class="mt-auto flex shrink-0"
openSettings={openSettings}
openHelp={() => platform.openLink(DOCS_URL)}
language={language}
/>
it — and Skill Hub/Settings/Help — to the bottom of the column.
The wrapper carries the auto margin so the fork's Skill Hub row can
sit directly above Settings without editing HomeUtilityNav. */}
<div class="mt-auto flex min-w-0 shrink-0 flex-col gap-1">
<div class="min-w-0 flex-col gap-1 pr-3 flex">
<SkillHubNav />
</div>
<HomeUtilityNav
class="flex shrink-0"
openSettings={openSettings}
openHelp={() => platform.openLink(DOCS_URL)}
language={language}
/>
</div>

{/* The DEV / BETA badge, moved out of the titlebar so Home can lead it.
The condition mirrors ChannelIndicator's own — kept here rather than
Expand Down
32 changes: 32 additions & 0 deletions packages/app/src/components/skill-hub-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Icon } from "@opencode-ai/ui/v2/icon"
import { useLocation, useNavigate } from "@solidjs/router"
import { Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { HOME_PROJECT_NAV_LABEL, HOME_PROJECT_NAV_ROW } from "@/pages/home"

// Fork-only: the sidebar's Skill Hub entry, rendered directly above Settings
// (see app-sidebar.tsx). Hidden entirely when the platform has no skillhub
// bridge (the web build), matching the page's desktop-only fallback.
export function SkillHubNav() {
const language = useLanguage()
const platform = usePlatform()
const location = useLocation()
const navigate = useNavigate()
const active = () => location.pathname === "/skills" || location.pathname.startsWith("/skills/")

return (
<Show when={platform.skillhub}>
<button
type="button"
data-component="skill-hub-nav"
data-selected={active() ? "" : undefined}
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
onClick={() => navigate("/skills")}
>
<Icon name="grid-plus" size="small" />
<span class={HOME_PROJECT_NAV_LABEL}>{language.t("sidebar.skillhub")}</span>
</button>
</Show>
)
}
4 changes: 4 additions & 0 deletions packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ServerConnection } from "./server"
import type { WslServersPlatform } from "../wsl/types"
import type { SandboxesPlatform } from "../sandbox/types"
import type { GatewaySsoPlatform } from "../gateway/types"
import type { SkillHubPlatform } from "../skillhub/types"
import type { UpdaterPlatform } from "../updater"

type PickerPaths = string | string[] | null
Expand Down Expand Up @@ -100,6 +101,9 @@ type PlatformBase = {
/** Gateway SSO login for the configure gate (Electron only) */
gatewaySso?: GatewaySsoPlatform

/** SkillHub registry browsing and installs (Electron only) */
skillhub?: SkillHubPlatform

/** Get the preferred display backend (desktop only) */
getDisplayBackend?(): Promise<DisplayBackend | null> | DisplayBackend | null

Expand Down
42 changes: 42 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,48 @@ export const dict = {
"sidebar.menu.toggle": "Toggle menu",
"sidebar.nav.projectsAndSessions": "Projects and sessions",
"sidebar.settings": "Settings",
"sidebar.skillhub": "Skill Hub",
"skillhub.title": "Skill Hub",
"skillhub.search.placeholder": "Search skills…",
"skillhub.sort.featured": "Featured",
"skillhub.sort.newest": "Newest",
"skillhub.sort.downloads": "Most downloaded",
"skillhub.sort.rating": "Top rated",
"skillhub.domain.all": "All",
"skillhub.domain.developer": "Developer",
"skillhub.domain.personal-assistant": "Personal assistant",
"skillhub.domain.workflow-builder": "Workflow builder",
"skillhub.empty": "No skills match your search.",
"skillhub.error.network": "Couldn't reach the Skill Hub. Check your connection and try again.",
"skillhub.error.unknown": "Something went wrong loading the Skill Hub.",
"skillhub.error.retry": "Retry",
"skillhub.page.label": "Page {{page}} of {{total}}",
"skillhub.page.previous": "Previous",
"skillhub.page.next": "Next",
"skillhub.detail.back": "Back to Skill Hub",
"skillhub.detail.install": "Install",
"skillhub.detail.openInHub": "Open in SkillHub",
"skillhub.detail.noReadme": "This skill has no SKILL.md to preview.",
"skillhub.detail.downloads": "downloads",
"skillhub.detail.ratings": "ratings",
"skillhub.install.title": "Install {{name}}",
"skillhub.install.scope.label": "Install to",
"skillhub.install.scope.project": "This project",
"skillhub.install.scope.global": "Global (all projects)",
"skillhub.install.agents.label": "For which agents?",
"skillhub.install.agents.locked": "Always included",
"skillhub.install.overwrite.title": "Already installed",
"skillhub.install.overwrite.description":
"This skill already exists at {{path}}. Overwrite it? Local edits will be lost.",
"skillhub.install.overwrite.confirm": "Overwrite",
"skillhub.install.cancel": "Cancel",
"skillhub.install.confirm": "Install",
"skillhub.install.installing": "Installing…",
"skillhub.install.success.title": "Installed {{name}}",
"skillhub.install.done": "Done",
"skillhub.desktopOnly.title": "Skill Hub is available in the desktop app",
"skillhub.desktopOnly.description":
"Browse and install skills from the CoDev desktop app, or use `codev skill` in the terminal.",
"sidebar.help": "Help",
"sidebar.pinned": "Pinned",
"sidebar.recents": "Recents",
Expand Down
Loading
Loading