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
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ sst-env.d.ts
packages/desktop/src/bindings.ts
packages/client/src/generated/
packages/client/src/generated-effect/
# Formatting realigns every row of the changed-files table, so a one-row edit
# churns the whole file — noise in the one place that records upstream-conflict guidance.
DIVERGENCES.md
12 changes: 0 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,6 @@ file is the human-readable summary of what changed and why.

### TUI

- **Pasting text works on Windows again** — on some Windows and WSL machines no
paste method reached the prompt at all: not `ctrl+v`, not right-click, not
Shift+Insert. Both routes that can deliver text end at the same clipboard
read — the `ctrl+v` keybind directly, and bracketed paste via the fallback the
prompt takes when the terminal hands it an empty paste — and that read went
through `clipboardy`, whose Windows backend is the only one that shells out to
a helper binary shipped inside its own package. That file does not exist in
our compiled single-file `codev.exe`, so every read failed silently and both
routes dead-ended. The clipboard is now read with PowerShell directly, the
same way copying already worked. A failed read also now says so instead of
doing nothing. Copying and pasting images were never affected.

- **Compact TODO panel above the prompt** — while the sidebar is hidden and the
session has unfinished todos, up to 4 items around the active one now render
directly above the input, with a summary line such as
Expand Down
37 changes: 28 additions & 9 deletions DIVERGENCES.md

Large diffs are not rendered by default.

13 changes: 13 additions & 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 @@ -47,6 +47,7 @@
"vite-plugin-solid": "catalog:"
},
"dependencies": {
"@codev/brand": "workspace:*",
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/components/dialog-custom-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Button } from "@opencode-ai/ui/button"
import { DOCS_URL } from "@codev/brand"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { IconButton } from "@opencode-ai/ui/icon-button"
Expand Down Expand Up @@ -184,7 +185,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
<p class="text-14-regular text-text-base">
{language.t("provider.custom.description.prefix")}
<Link href="https://netmind.viettel.vn/codev/docs/providers/#custom-provider" tabIndex={-1}>
<Link href={`${DOCS_URL}/providers#custom-provider`} tabIndex={-1}>
{language.t("provider.custom.description.link")}
</Link>
{language.t("provider.custom.description.suffix")}
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { DOCS_URL } from "@codev/brand"
import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
Expand Down Expand Up @@ -479,7 +480,7 @@ export const SettingsGeneral: Component = () => {
description={
<>
{language.t("settings.general.row.theme.description")}{" "}
<Link href="https://netmind.viettel.vn/codev/docs/themes/">{language.t("common.learnMore")}</Link>
<Link href={`${DOCS_URL}/themes`}>{language.t("common.learnMore")}</Link>
</>
}
>
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/components/settings-v2/general.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, Show, createMemo, createResource, onMount } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { DOCS_URL } from "@codev/brand"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
Expand Down Expand Up @@ -438,7 +439,7 @@ export const SettingsGeneralV2: Component<{
description={
<>
{language.t("settings.general.row.theme.description")}{" "}
<Link class="settings-v2-link" href="https://netmind.viettel.vn/codev/docs/themes/">
<Link class="settings-v2-link" href={`${DOCS_URL}/themes`}>
{language.t("common.learnMore")}
</Link>
</>
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/desktop-menu.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DOCS_URL } from "@codev/brand"

export type DesktopMenuPlatform = "macos" | "windows"

export type DesktopMenuAction =
Expand Down Expand Up @@ -200,7 +202,7 @@ export const DESKTOP_MENU: DesktopMenu[] = [
id: "help",
label: "Help",
items: [
{ type: "item", label: "CoDev Code Documentation", href: "https://netmind.viettel.vn/codev/docs" },
{ type: "item", label: "CoDev Code Documentation", href: DOCS_URL },
{ type: "item", label: "Export Logs...", command: "logs.export" },
{ type: "separator" },
{
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Switch,
} from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { DOCS_URL } from "@codev/brand"
import { createStore, produce } from "solid-js/store"
import { useQuery } from "@tanstack/solid-query"
import { Button } from "@opencode-ai/ui/button"
Expand Down Expand Up @@ -673,7 +674,7 @@ export function NewHome() {
clearNotifications={clearNotifications}
unseenCount={unseenCount}
openSettings={openSettings}
openHelp={() => platform.openLink("https://netmind.viettel.vn/codev/docs")}
openHelp={() => platform.openLink(DOCS_URL)}
language={language}
onWheel={(event) => {
if (sessionViewport) containHomeWheel(event, sessionViewport)
Expand Down Expand Up @@ -780,7 +781,7 @@ export function NewHome() {
<HomeUtilityNav
class="flex lg:hidden"
openSettings={openSettings}
openHelp={() => platform.openLink("https://netmind.viettel.vn/codev/docs")}
openHelp={() => platform.openLink(DOCS_URL)}
language={language}
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type Accessor,
} from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { DOCS_URL } from "@codev/brand"
import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout"
import { useServerSync } from "@/context/server-sync"
Expand Down Expand Up @@ -2238,7 +2239,7 @@ export default function LegacyLayout(props: ParentProps) {
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://netmind.viettel.vn/codev/docs")}
onOpenHelp={() => platform.openLink(DOCS_URL)}
renderPanel={() =>
mobile ? <SidebarPanel project={currentProject} mobile /> : <SidebarPanel project={currentProject} merged />
}
Expand Down
17 changes: 17 additions & 0 deletions packages/codev-brand/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@codev/brand",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "bun test"
},
"devDependencies": {
"@types/bun": "catalog:"
},
"exports": {
".": "./src/index.ts"
}
}
68 changes: 68 additions & 0 deletions packages/codev-brand/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export const PRODUCT_NAME = "CoDev Code"
export const COMMAND_NAME = "codev"
// Base64 (like codev-gateway's const.ts) to keep the host out of casual greps,
// not as a secret — the decoded URL ships in the system prompt the model reads.
export const DOCS_URL = atob("aHR0cHM6Ly9uZXRtaW5kLnZpZXR0ZWwudm4vY29kZXYvZW4vZG9jcy9jb2Rl")
export const REPO_URL = "https://github.com/quickbeard/codev-code"
export const ISSUES_URL = "https://github.com/quickbeard/codev-code/issues"

// Upstream names that must survive verbatim. Package ids and the config JSON
// Schema URL are real identifiers the fork deliberately keeps (see "Known
// rebrand gaps" in DIVERGENCES.md), so a blind rename would break the model's
// advice: it would tell users to `import "@codev/plugin"` or point their editor
// at a schema URL that does not exist. Trade-off: prose that hyphenates the
// upstream name ("opencode-managed") is preserved too and stays upstream-branded.
const PRESERVED =
/(@opencode-ai\/[\w.-]+|https:\/\/opencode\.ai\/config\.json|opencode-[\w][\w.@-]*|opencode:\/\/|ai\.opencode\.[\w]+|api\.opencode\.ai|opencode\.ai\/(?:zen|go|auth|s\/|changelog\.json)|opencode\.db)/g

// Longest match first: the URL rules have to consume their hosts before the
// bare-name rule can rewrite the word inside them.
const RULES: readonly (readonly [RegExp, string])[] = [
[/https:\/\/github\.com\/anomalyco\/opencode\/issues/g, ISSUES_URL],
[/https:\/\/github\.com\/anomalyco\/opencode/g, REPO_URL],
[/https:\/\/opencode\.ai(?:\/docs)?/g, DOCS_URL],
[/\bopencode\.ai\b/g, DOCS_URL.replace("https://", "")],
[/\bopencode\.jsonc\b/g, "codev.jsonc"],
[/\bopencode\.json\b/g, "codev.json"],
[/\.opencode\b/g, ".codev"],
// Command invocations keep the binary name; everything else is the product name.
[
/\bopencode (?=(?:auth|run|serve|models|agent|plugin|mcp|upgrade|tui|github|configure|codegraph|debug|stats)\b)/g,
`${COMMAND_NAME} `,
],
[/\bOpenCode\b/g, PRODUCT_NAME],
[/\bopencode\b/g, PRODUCT_NAME],
]

// Inputs are module-level prompt constants and tool descriptions, so the key
// space is bounded by the number of distinct prompts loaded in a process.
const cache = new Map<string, string>()

// `split` on a regex with one capture group interleaves the matched spans at odd
// indices, so preserved names pass through without ever being offered to RULES.
const partition = (text: string) => text.split(PRESERVED)

/** Rewrite upstream product names in model-visible text to the fork's. */
export function rebrand(text: string) {
const cached = cache.get(text)
if (cached !== undefined) return cached
const result = partition(text)
.map((part, index) =>
index % 2 === 1 ? part : RULES.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), part),
)
.join("")
cache.set(text, result)
return result
}

/**
* Lines still carrying the upstream name after {@link rebrand}, ignoring
* preserved identifiers. The guard test fails on a non-empty result, so an
* upstream sync that introduces a new brand phrasing surfaces as a test failure
* instead of shipping silently.
*/
export function brandLeaks(text: string) {
return partition(rebrand(text))
.filter((_, index) => index % 2 === 0)
.flatMap((part) => part.split("\n").filter((line) => /opencode/i.test(line)))
}
60 changes: 60 additions & 0 deletions packages/codev-brand/test/brand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, test } from "bun:test"
import { brandLeaks, DOCS_URL, ISSUES_URL, PRODUCT_NAME, rebrand } from "../src"

describe("rebrand", () => {
test("rewrites the identity line", () => {
expect(rebrand("You are opencode, an interactive CLI tool that helps users.")).toBe(
`You are ${PRODUCT_NAME}, an interactive CLI tool that helps users.`,
)
expect(rebrand("You are OpenCode, the best coding agent on the planet.")).toBe(
`You are ${PRODUCT_NAME}, the best coding agent on the planet.`,
)
})

test("routes support links at the fork", () => {
expect(rebrand("report the issue at https://github.com/anomalyco/opencode/issues")).toBe(
`report the issue at ${ISSUES_URL}`,
)
expect(rebrand("report the issue at\n https://github.com/anomalyco/opencode")).toBe(
"report the issue at\n https://github.com/quickbeard/codev-code",
)
expect(rebrand("docs at https://opencode.ai/docs")).toBe(`docs at ${DOCS_URL}`)
expect(rebrand("docs at https://opencode.ai")).toBe(`docs at ${DOCS_URL}`)
})

test("preserves identifiers the fork deliberately keeps", () => {
const preserved = [
'import type { Plugin } from "@opencode-ai/plugin"',
'"$schema": "https://opencode.ai/config.json"',
'"opencode-gemini-auth", "opencode-foo@1.2.3", ["opencode-bar", { "option": "value" }]',
"the opencode:// deep link and the ai.opencode.desktop appId",
"state lives in opencode.db",
]
for (const line of preserved) expect(rebrand(line)).toBe(line)
})

test("renames config files and directories", () => {
expect(rebrand("`./opencode.json`, `./opencode.jsonc`, or `.opencode/opencode.json`")).toBe(
"`./codev.json`, `./codev.jsonc`, or `.codev/codev.json`",
)
})

test("keeps the binary name in command invocations", () => {
expect(rebrand("run `opencode auth login`, then restart opencode")).toBe(
`run \`codev auth login\`, then restart ${PRODUCT_NAME}`,
)
})

test("is idempotent", () => {
const once = rebrand("You are opencode. Docs: https://opencode.ai/docs")
expect(rebrand(once)).toBe(once)
})
})

describe("brandLeaks", () => {
test("reports lines the rules did not cover", () => {
expect(brandLeaks("You are opencode.")).toEqual([])
expect(brandLeaks('import { x } from "@opencode-ai/core"')).toEqual([])
expect(brandLeaks("Ask the OPENCODE team.")).toEqual(["Ask the OPENCODE team."])
})
})
8 changes: 8 additions & 0 deletions packages/codev-brand/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext"],
"noUncheckedIndexedAccess": false
}
}
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.102",
"@aws-sdk/credential-providers": "3.1057.0",
"@codev/brand": "workspace:*",
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/plugin/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
export * as SkillPlugin from "./skill"

import { define } from "./internal"
import { rebrand } from "@codev/brand"
import { Effect } from "effect"
import { AbsolutePath } from "../schema"
import { SkillV2 } from "../skill"
import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" }

export const CustomizeOpencodeContent = customizeOpencodeContent
// The markdown body stays byte-identical to upstream; product names are rewritten
// on the way out so upstream edits to it keep applying cleanly.
export const CustomizeOpencodeContent = rebrand(customizeOpencodeContent)

export const Plugin = define({
id: "skill",
Expand All @@ -18,10 +21,10 @@ export const Plugin = define({
SkillV2.EmbeddedSource.make({
type: "embedded",
skill: SkillV2.Info.make({
name: "customize-opencode",
name: "customize-codev",
description:
"Use ONLY when the user is editing or creating opencode's own configuration: codev.json, codev.jsonc, files under .codev/, or files under ~/.config/codev/. Also use when creating or fixing opencode agents, subagents, commands, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.",
location: AbsolutePath.make("/builtin/customize-opencode.md"),
"Use ONLY when the user is editing or creating CoDev Code's own configuration: codev.json, codev.jsonc, files under .codev/, or files under ~/.config/codev/. Also use when creating or fixing CoDev Code agents, subagents, commands, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring CoDev Code itself.",
location: AbsolutePath.make("/builtin/customize-codev.md"),
content: CustomizeOpencodeContent,
}),
}),
Expand Down
Loading
Loading