fix: address upstream merge review findings

This commit is contained in:
Johnny Eric Amancio
2026-08-06 00:12:23 +02:00
parent 25f4b58d93
commit cbbbd7217f
36 changed files with 171 additions and 739 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Adopt OpenCode v1.18.0 improvements, including code mode, expanded model reasoning controls, MCP reliability updates, and TUI enhancements.
+4
View File
@@ -142,6 +142,10 @@ jobs:
# kilocode_change end
# kilocode_change start - test non-CLI packages separately from sharded CLI tests
- name: Verify package test scheduling
if: matrix.settings.run && matrix.settings.packages && matrix.settings.os == 'linux'
run: bun run script/check-test-ci.ts
- name: Run non-CLI unit tests
if: matrix.settings.run && matrix.settings.packages
run: bun turbo test:ci --output-logs=errors-only --log-order=grouped --log-prefix=task --filter='!@kilocode/cli' --filter='!@kilocode/kilo-jetbrains'
-1
View File
@@ -16,7 +16,6 @@
"prepare": "husky",
"random": "echo 'Random script'",
"sso": "aws sso login --sso-session=opencode --no-browser",
"translate:app": "bun run script/translate-app.ts",
"test": "echo 'do not run tests from root' && exit 1",
"extension": "bun --cwd packages/kilo-vscode script/launch.ts",
"extension:isolated": "bun --cwd packages/kilo-vscode script/launch.ts --isolated",
@@ -1297,6 +1297,9 @@ function reasoningEffort(model: Provider.Model, effort: string) {
function anthropicEffort(model: Provider.Model, effort: string) {
if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
// kilocode_change start - Kimi Anthropic endpoints require adaptive thinking summaries for published effort tiers
if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort }
// kilocode_change end
if (!anthropicAdaptiveEfforts(model.api.id)) return
return {
thinking: {
@@ -1307,6 +1310,20 @@ function anthropicEffort(model: Provider.Model, effort: string) {
}
}
// kilocode_change start
function isKimiFamily(model: Provider.Model) {
if (
[model.providerID, model.api.id].some((id) => {
const value = id.toLowerCase()
return value.includes("kimi") || value.includes("moonshot")
})
)
return true
const url = model.api.url.toLowerCase()
return ["api.kimi.com", "api.moonshot.ai", "api.moonshot.cn", "api.moonshotai.cn"].some((host) => url.includes(host))
}
// kilocode_change end
function reasoningBudget(model: Provider.Model, budget: number) {
switch (model.api.npm) {
case "@openrouter/ai-sdk-provider":
@@ -1,20 +1,20 @@
You are OpenCode, the best coding agent on the planet.
You are Kilo, the best coding agent on the planet.
You are based on a large language model trained by Meta MSL named Muse Spark.
When asked who you are, identify yourself as OpenCode powered by Meta Muse Spark by name.
When asked who you are, identify yourself as Kilo powered by Meta Muse Spark by name.
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- ctrl+p to list available actions
- To give feedback, users should report the issue at
https://github.com/Kilo-Org/kilocode
When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from OpenCode docs. The list of available docs is available at https://opencode.ai/docs
When the user directly asks about Kilo (eg. "can Kilo do...", "does Kilo have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific Kilo feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from Kilo docs. The list of available docs is available at https://kilo.ai/docs
# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
# Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Kilo honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Task Management
You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import type { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
const model = {
reasoning_options: [{ type: "effort", values: ["low", "high", "max"] }],
} as unknown as ModelsDev.Model
function target(input: { providerID: string; id: string; url: string }) {
return {
id: input.id,
providerID: input.providerID,
api: { id: input.id, npm: "@ai-sdk/anthropic", url: input.url },
capabilities: { reasoning: true },
limit: { output: 64_000 },
} as unknown as Provider.Model
}
describe("Kimi adaptive effort", () => {
test("uses adaptive summarized thinking for Kimi model IDs", () => {
const variants = ProviderTransform.reasoningVariants(
model,
target({ providerID: "moonshotai", id: "kimi-k3", url: "https://example.test/v1" }),
)
expect(variants).toEqual({
low: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
high: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
max: { thinking: { type: "adaptive", display: "summarized" }, effort: "max" },
})
})
test("recognizes custom Kimi providers by Moonshot API host", () => {
const variants = ProviderTransform.reasoningVariants(
model,
target({ providerID: "custom", id: "custom-model", url: "https://api.moonshot.ai/anthropic" }),
)
expect(variants?.high).toEqual({ thinking: { type: "adaptive", display: "summarized" }, effort: "high" })
})
})
@@ -0,0 +1,11 @@
import { expect, test } from "bun:test"
import type { Provider } from "@/provider/provider"
import { SystemPrompt } from "@/session/system"
test("Muse Spark identifies as Kilo and uses Kilo documentation", () => {
const prompt = SystemPrompt.provider({ api: { id: "meta/muse-spark-preview" } } as Provider.Model)[0]
expect(prompt).toContain("Kilo powered by Meta Muse Spark")
expect(prompt).toContain("https://kilo.ai/docs")
expect(prompt).not.toContain("identify yourself as OpenCode")
expect(prompt).not.toContain("https://opencode.ai/docs")
})
+1 -1
View File
@@ -72,7 +72,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "تم الوصول إلى الحد المجاني",
"dialog.usageExceeded.freeTier.description":
"اشترك في Kilo Go للحصول على وصول موثوق إلى أفضل النماذج مفتوحة المصدر، ابتداءً من $5/شهر.",
"اشترك في Kilo Go للحصول على وصول موثوق إلى أفضل النماذج مفتوحة المصدر، ابتداءً من $5/شهر.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "اشترك",
"dialog.usageExceeded.accountRateLimit.title": "تم الوصول إلى حد Go",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -70,7 +70,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Limite gratuito atingido",
"dialog.usageExceeded.freeTier.description":
"Assine o Kilo Go para ter acesso confiável aos melhores modelos open-source, a partir de $5/mês.",
"Assine o Kilo Go para ter acesso confiável aos melhores modelos open-source, a partir de $5/mês.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Assinar",
"dialog.usageExceeded.accountRateLimit.title": "Limite do Go atingido",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -74,7 +74,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Dostignut besplatan limit",
"dialog.usageExceeded.freeTier.description":
"Pretplatite se na Kilo Go za pouzdan pristup najboljim open-source modelima, počevši od $5/mjesec.",
"Pretplatite se na Kilo Go za pouzdan pristup najboljim open-source modelima, počevši od $5/mjesec.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Pretplati se",
"dialog.usageExceeded.accountRateLimit.title": "Dostignut Go limit",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -70,7 +70,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Gratis grænse nået",
"dialog.usageExceeded.freeTier.description":
"Abonnér på Kilo Go for pålidelig adgang til de bedste open source-modeller, fra $5/måned.",
"Abonnér på Kilo Go for pålidelig adgang til de bedste open source-modeller, fra $5/måned.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Abonnér",
"dialog.usageExceeded.accountRateLimit.title": "Go-grænse nået",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -78,7 +78,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Kostenloses Limit erreicht",
"dialog.usageExceeded.freeTier.description":
"Abonniere Kilo Go für zuverlässigen Zugriff auf die besten Open-Source-Modelle, ab $5/Monat.",
"Abonniere Kilo Go für zuverlässigen Zugriff auf die besten Open-Source-Modelle, ab $5/Monat.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Abonnieren",
"dialog.usageExceeded.accountRateLimit.title": "Go-Limit erreicht",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -74,7 +74,7 @@ export const dict: Record<string, string> = {
"dialog.usageExceeded.freeTier.title": "Free limit reached",
"dialog.usageExceeded.freeTier.description":
"Subscribe to Kilo Go for reliable access to the best open-source models, starting at $5/month.",
"Subscribe to Kilo Go for reliable access to the best open-source models, starting at $5/month.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Subscribe",
"dialog.usageExceeded.accountRateLimit.title": "Go limit reached",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -70,7 +70,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Límite gratuito alcanzado",
"dialog.usageExceeded.freeTier.description":
"Suscríbete a Kilo Go para acceso fiable a los mejores modelos de código abierto, desde $5/mes.",
"Suscríbete a Kilo Go para acceso fiable a los mejores modelos de código abierto, desde $5/mes.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Suscribirse",
"dialog.usageExceeded.accountRateLimit.title": "Límite de Go alcanzado",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -70,7 +70,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Limite gratuite atteinte",
"dialog.usageExceeded.freeTier.description":
"Abonnez-vous à Kilo Go pour un accès fiable aux meilleurs modèles open source, à partir de $5/mois.",
"Abonnez-vous à Kilo Go pour un accès fiable aux meilleurs modèles open source, à partir de $5/mois.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "S'abonner",
"dialog.usageExceeded.accountRateLimit.title": "Limite Go atteinte",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -75,7 +75,7 @@ export const dict: Record<string, string> = {
"dialog.usageExceeded.freeTier.title": "Limite gratuito raggiunto",
"dialog.usageExceeded.freeTier.description":
"Abbonati a Kilo Go per un accesso affidabile ai migliori modelli open source, a partire da $5 al mese.",
"Abbonati a Kilo Go per un accesso affidabile ai migliori modelli open source, a partire da $5 al mese.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Abbonati",
"dialog.usageExceeded.accountRateLimit.title": "Limite Go raggiunto",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -70,7 +70,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "無料制限に達しました",
"dialog.usageExceeded.freeTier.description":
"Kilo Go にサブスクライブして、最高のオープンソースモデルに安定してアクセスできます。月額 $5 から。",
"Kilo Go にサブスクライブして、最高のオープンソースモデルに安定してアクセスできます。月額 $5 から。", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "サブスクライブ",
"dialog.usageExceeded.accountRateLimit.title": "Go の制限に達しました",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -52,7 +52,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "무료 한도에 도달했습니다",
"dialog.usageExceeded.freeTier.description":
"Kilo Go를 구독하여 최고의 오픈 소스 모델에 안정적으로 액세스하세요. 월 $5부터 시작합니다.",
"Kilo Go를 구독하여 최고의 오픈 소스 모델에 안정적으로 액세스하세요. 월 $5부터 시작합니다.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "구독",
"dialog.usageExceeded.accountRateLimit.title": "Go 한도에 도달했습니다",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -76,7 +76,7 @@ export const dict: Record<string, string> = {
// kilocode_change start - complete upstream usage-exceeded translations
"dialog.usageExceeded.freeTier.title": "Gratis limiet bereikt",
"dialog.usageExceeded.freeTier.description":
"Abonneer je op Kilo Go voor betrouwbare toegang tot de beste open-sourcemodellen, vanaf $5 per maand.",
"Abonneer je op Kilo Go voor betrouwbare toegang tot de beste open-sourcemodellen, vanaf $5 per maand.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Abonneren",
"dialog.usageExceeded.accountRateLimit.title": "Go-limiet bereikt",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -57,7 +57,7 @@ export const dict: Record<Keys, string> = {
"dialog.usageExceeded.freeTier.title": "Gratis grense nådd",
"dialog.usageExceeded.freeTier.description":
"Abonner på Kilo Go for pålitelig tilgang til de beste åpen kildekode-modellene, fra $5/måned.",
"Abonner på Kilo Go for pålitelig tilgang til de beste åpen kildekode-modellene, fra $5/måned.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Abonner",
"dialog.usageExceeded.accountRateLimit.title": "Go-grense nådd",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -71,7 +71,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Osiągnięto limit darmowy",
"dialog.usageExceeded.freeTier.description":
"Subskrybuj Kilo Go, aby uzyskać niezawodny dostęp do najlepszych modeli open source, od $5/miesiąc.",
"Subskrybuj Kilo Go, aby uzyskać niezawodny dostęp do najlepszych modeli open source, od $5/miesiąc.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Subskrybuj",
"dialog.usageExceeded.accountRateLimit.title": "Osiągnięto limit Go",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -71,7 +71,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Достигнут бесплатный лимит",
"dialog.usageExceeded.freeTier.description":
"Подпишитесь на Kilo Go для надёжного доступа к лучшим моделям с открытым исходным кодом, от $5/месяц.",
"Подпишитесь на Kilo Go для надёжного доступа к лучшим моделям с открытым исходным кодом, от $5/месяц.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Подписаться",
"dialog.usageExceeded.accountRateLimit.title": "Достигнут лимит Go",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -71,7 +71,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "ถึงขีดจำกัดฟรีแล้ว",
"dialog.usageExceeded.freeTier.description":
"สมัครสมาชิก Kilo Go เพื่อการเข้าถึงโมเดลโอเพนซอร์สที่ดีที่สุดอย่างเชื่อถือได้ เริ่มต้นที่ $5/เดือน",
"สมัครสมาชิก Kilo Go เพื่อการเข้าถึงโมเดลโอเพนซอร์สที่ดีที่สุดอย่างเชื่อถือได้ เริ่มต้นที่ $5/เดือน", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "สมัครสมาชิก",
"dialog.usageExceeded.accountRateLimit.title": "ถึงขีดจำกัดของ Go แล้ว",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -79,7 +79,7 @@ export const dict = {
"dialog.usageExceeded.freeTier.title": "Ücretsiz sınıra ulaşıldı",
"dialog.usageExceeded.freeTier.description":
"En iyi açık kaynak modellere güvenilir erişim için Kilo Go'ya abone olun. Aylık $5'tan başlar.",
"En iyi açık kaynak modellere güvenilir erişim için Kilo Go'ya abone olun. Aylık $5'tan başlar.", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "Abone ol",
"dialog.usageExceeded.accountRateLimit.title": "Go sınırına ulaşıldı",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -75,7 +75,7 @@ export const dict = {
"ui.sessionTurn.error.addCredits": "添加积分",
"dialog.usageExceeded.freeTier.title": "免费额度已用完",
"dialog.usageExceeded.freeTier.description": "订阅 Kilo Go,可靠地使用最佳开源模型,每月 $5 起。",
"dialog.usageExceeded.freeTier.description": "订阅 Kilo Go,可靠地使用最佳开源模型,每月 $5 起。", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "订阅",
"dialog.usageExceeded.accountRateLimit.title": "Go 额度已用完",
"dialog.usageExceeded.accountRateLimit.description":
+1 -1
View File
@@ -75,7 +75,7 @@ export const dict = {
"ui.sessionTurn.error.addCredits": "新增點數",
"dialog.usageExceeded.freeTier.title": "已達免費額度上限",
"dialog.usageExceeded.freeTier.description": "訂閱 Kilo Go,可靠地使用最佳開源模型,每月 $5 起。",
"dialog.usageExceeded.freeTier.description": "訂閱 Kilo Go,可靠地使用最佳開源模型,每月 $5 起。", // kilocode_change
"dialog.usageExceeded.freeTier.actionLabel": "訂閱",
"dialog.usageExceeded.accountRateLimit.title": "已達 Go 額度上限",
"dialog.usageExceeded.accountRateLimit.description":
+35
View File
@@ -0,0 +1,35 @@
// kilocode_change - new file
import path from "path"
const root = path.resolve(import.meta.dir, "..")
const proc = Bun.spawnSync(["git", "ls-files", "packages"], {
cwd: root,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
if (proc.exitCode !== 0) throw new Error(proc.stderr.toString() || "Unable to list tracked package tests")
const exempt = new Set(["packages/kilo-vscode"])
const dirs = new Set(
proc.stdout
.toString()
.split("\n")
.filter((file) => /\.test\.tsx?$/.test(file))
.map((file) => file.split("/").slice(0, 2).join("/")),
)
const missing: string[] = []
for (const dir of [...dirs].sort()) {
if (exempt.has(dir)) continue
const file = path.join(root, dir, "package.json")
const source = Bun.file(file)
if (!(await source.exists())) continue
const pkg = (await source.json()) as { scripts?: Record<string, string> }
const scripts = pkg.scripts
if (!scripts?.test && !scripts?.["test:ci"]) continue
if (!scripts["test:ci"]) missing.push(`${dir}/package.json`)
}
if (missing.length > 0) throw new Error(`Test-bearing packages missing test:ci:\n${missing.join("\n")}`)
console.log(`check-test-ci: ok (${dirs.size - exempt.size} test-bearing package(s))`)
-21
View File
@@ -1,21 +0,0 @@
Translate the product app locale `$1` from the English source dictionaries. English is the read-only source of truth. Its copy is intentional and must never be modified, rewritten, or "improved."
The translation request below contains the locale glossary, exact source and target files, plus missing, extra, and placeholder-mismatched keys.
```json
$ARGUMENTS
```
Requirements:
- Edit only the target files listed in the request. Never edit English, another locale, tests, registries, docs, or other packages.
- Treat every English key and value as intentional. Translate from it without changing the English source files in any way.
- Add every missing key with a natural, concise translation suitable for application UI.
- Remove keys listed as extra and repair values listed under `placeholders` so their `{{tokens}}` exactly match English.
- Preserve existing translations unless they have a listed placeholder mismatch.
- Preserve meaning, intent, tone, capitalization, punctuation, whitespace, and formatting.
- Preserve technical terms and artifacts exactly: OpenCode, API names, identifiers, code, commands, flags, paths, URLs, versions, error messages, config keys, and placeholder tokens.
- Apply the locale glossary included in the request.
- `ui.sessionTurn.diffs.changed.one` and `ui.sessionTurn.diffs.changed.other` are complete count phrases. Preserve `{{count}}` and translate the whole phrase naturally rather than composing translated fragments.
- Use only read, glob, grep, and edit tools. Do not run commands or delegate work.
- Finish only when every requested key is synchronized and no other file has changed.
-168
View File
@@ -1,168 +0,0 @@
import { describe, expect, test } from "bun:test"
import {
findDrift,
glossaryFile,
modelVariants,
parseTranslationArgs,
runPool,
sessionIDFromEvents,
sessionModels,
targetFiles,
textFromEvents,
translationConfig,
unexpectedChanges,
} from "./translate-app"
describe("translate app", () => {
test("parses one locale with the public model defaults", () => {
expect(parseTranslationArgs(["fr"])).toEqual({
target: "fr",
concurrency: 1,
model: "opencode/gpt-5.5",
variant: "xhigh",
dryRun: false,
check: false,
help: false,
})
})
test("parses all locales with bounded concurrency overrides", () => {
expect(
parseTranslationArgs([
"all",
"--concurrency",
"7",
"--model",
"opencode/gpt-5.4",
"--variant",
"high",
"--dry-run",
]),
).toEqual({
target: "all",
concurrency: 7,
model: "opencode/gpt-5.4",
variant: "high",
dryRun: true,
check: false,
help: false,
})
})
test("rejects unsupported targets and invalid concurrency", () => {
expect(() => parseTranslationArgs(["en"])).toThrow("Unknown locale")
expect(() => parseTranslationArgs(["fr", "de"])).toThrow("one locale")
expect(() => parseTranslationArgs(["all", "--concurrency", "0"])).toThrow("positive integer")
})
test("parses fresh-process parity checks without requesting translation", () => {
expect(parseTranslationArgs(["fr", "--check"]).check).toBe(true)
})
test("limits each locale to its app surfaces", () => {
expect(targetFiles("fr")).toEqual([
"packages/app/src/i18n/fr.ts",
"packages/ui/src/i18n/fr.ts",
"packages/desktop/src/renderer/i18n/fr.ts",
])
expect(targetFiles("tr")).toEqual(["packages/app/src/i18n/tr.ts", "packages/ui/src/i18n/tr.ts"])
})
test("maps product locale codes to their glossaries", () => {
expect(glossaryFile("fr")).toBe(".opencode/glossary/fr.md")
expect(glossaryFile("zh")).toBe(".opencode/glossary/zh-cn.md")
expect(glossaryFile("zht")).toBe(".opencode/glossary/zh-tw.md")
})
test("finds key and placeholder drift", () => {
expect(
findDrift(
{ keep: "Hello {{name}}", missing: "Missing", changed: "{{one}} {{two}}" },
{ keep: "Bonjour {{name}}", extra: "Extra", changed: "{{one}}" },
),
).toEqual({ missing: ["missing"], extra: ["extra"], placeholders: ["changed"] })
})
test("runs work with the requested maximum concurrency", async () => {
const active = new Set<number>()
const peaks: number[] = []
const result = await runPool([1, 2, 3, 4, 5], 2, async (item) => {
active.add(item)
peaks.push(active.size)
await Bun.sleep(5)
active.delete(item)
return item * 2
})
expect(result).toEqual([2, 4, 6, 8, 10])
expect(Math.max(...peaks)).toBe(2)
})
test("reads the actual model and variant from the completed session", () => {
expect(sessionIDFromEvents('shared: https://example.test\n{"type":"step_start","sessionID":"ses_test"}\n')).toBe(
"ses_test",
)
expect(
sessionModels({
messages: [
{ info: { role: "user" } },
{
info: {
role: "assistant",
providerID: "opencode",
modelID: "gpt-5.5",
variant: "xhigh",
},
},
],
}),
).toEqual([{ model: "opencode/gpt-5.5", variant: "xhigh" }])
expect(
textFromEvents(
'shared: https://example.test\n{"type":"text","sessionID":"ses_test","part":{"text":"finished"}}\n',
),
).toBe("finished")
})
test("resolves variants from verbose model output", () => {
const output = `opencode/other
{"variants":{}}
opencode/gpt-5.5
{"variants":{"high":{"reasoningEffort":"high"},"xhigh":{"reasoningEffort":"xhigh"}}}
opencode/next
{"variants":{}}
`
expect(modelVariants(output, "opencode/gpt-5.5")).toEqual({
high: { reasoningEffort: "high" },
xhigh: { reasoningEffort: "xhigh" },
})
})
test("disables side effects and scopes edits for the translation agent", () => {
const config = translationConfig("translate-app-fr", "opencode/gpt-5.5", ["packages/app/src/i18n/fr.ts"])
expect(config.share).toBe("disabled")
expect(config.formatter).toBe(false)
expect(config.lsp).toBe(false)
expect(config.agent["translate-app-fr"].permission.edit).toEqual({
"*": "deny",
"packages/app/src/i18n/fr.ts": "allow",
})
})
test("detects edits outside the locale targets", () => {
expect(
unexpectedChanges(
{ "script/translate-app.ts": "before" },
{
"script/translate-app.ts": "before",
"packages/app/src/i18n/fr.ts": "translated",
"packages/app/src/app.tsx": "unexpected",
},
["packages/app/src/i18n/fr.ts"],
),
).toEqual(["packages/app/src/app.tsx"])
expect(unexpectedChanges({ "already-dirty.ts": "before" }, { "already-dirty.ts": "after" }, [])).toEqual([
"already-dirty.ts",
])
})
})
-523
View File
@@ -1,523 +0,0 @@
#!/usr/bin/env bun
import path from "path"
import { parseArgs } from "util"
import { pathToFileURL } from "url"
const locales = [
"ar",
"br",
"bs",
"da",
"de",
"es",
"fr",
"ja",
"ko",
"no",
"pl",
"ru",
"uk",
"th",
"tr",
"zh",
"zht",
] as const
type Locale = (typeof locales)[number]
const languages = {
ar: "Arabic",
br: "Brazilian Portuguese",
bs: "Bosnian",
da: "Danish",
de: "German",
es: "Spanish",
fr: "French",
ja: "Japanese",
ko: "Korean",
no: "Norwegian Bokmal",
pl: "Polish",
ru: "Russian",
uk: "Ukrainian",
th: "Thai",
tr: "Turkish",
zh: "Simplified Chinese",
zht: "Traditional Chinese",
} as const satisfies Record<Locale, string>
type Dictionary = Record<string, string>
type Drift = ReturnType<typeof findDrift>
type Domain = { name: string; source: string; target: string; drift: Drift }
const desktopLocales = new Set<Locale>(locales.filter((locale) => locale !== "th" && locale !== "tr"))
const root = path.resolve(import.meta.dir, "..")
export function parseTranslationArgs(args: string[]) {
const parsed = parseArgs({
args,
options: {
concurrency: { type: "string", short: "c", default: "4" },
model: { type: "string", default: "opencode/gpt-5.5" },
variant: { type: "string", default: "xhigh" },
"dry-run": { type: "boolean", default: false },
check: { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
allowPositionals: true,
})
const target = parsed.positionals[0] ?? "all"
const concurrency = Number(parsed.values.concurrency)
if (!parsed.values.help && parsed.positionals.length !== 1) throw new Error("Pass one locale or 'all'.")
if (target !== "all" && !isLocale(target)) throw new Error(`Unknown locale '${target}'.`)
if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error("Concurrency must be a positive integer.")
return {
target,
concurrency: target === "all" ? concurrency : 1,
model: parsed.values.model,
variant: parsed.values.variant,
dryRun: parsed.values["dry-run"],
check: parsed.values.check,
help: parsed.values.help,
}
}
export function targetFiles(locale: Locale) {
return [
`packages/app/src/i18n/${locale}.ts`,
`packages/ui/src/i18n/${locale}.ts`,
...(desktopLocales.has(locale) ? [`packages/desktop/src/renderer/i18n/${locale}.ts`] : []),
]
}
export function glossaryFile(locale: Locale) {
if (locale === "zh") return ".opencode/glossary/zh-cn.md"
if (locale === "zht") return ".opencode/glossary/zh-tw.md"
return `.opencode/glossary/${locale}.md`
}
export function findDrift(source: Dictionary, target: Dictionary) {
return {
missing: Object.keys(source).filter((key) => !Object.hasOwn(target, key)),
extra: Object.keys(target).filter((key) => !Object.hasOwn(source, key)),
placeholders: Object.keys(source).filter(
(key) => Object.hasOwn(target, key) && tokens(source[key]).join() !== tokens(target[key]).join(),
),
}
}
export function sessionIDFromEvents(output: string) {
const match = output.match(/"sessionID"\s*:\s*"([^"]+)"/)
if (!match?.[1]) throw new Error("OpenCode did not report a session ID.")
return match[1]
}
export function sessionModels(value: unknown) {
if (!isRecord(value) || !Array.isArray(value.messages))
throw new Error("OpenCode returned an invalid session export.")
return value.messages.flatMap((message) => {
if (!isRecord(message) || !isRecord(message.info) || message.info.role !== "assistant") return []
if (typeof message.info.providerID !== "string" || typeof message.info.modelID !== "string") {
throw new Error("OpenCode session export omitted the assistant model.")
}
return [
{
model: `${message.info.providerID}/${message.info.modelID}`,
variant: typeof message.info.variant === "string" ? message.info.variant : undefined,
},
]
})
}
export function modelVariants(output: string, model: string) {
const normalized = output.replaceAll("\r\n", "\n")
const marker = `${model}\n`
const start = normalized.indexOf(marker)
if (start < 0) throw new Error(`Model not found: ${model}`)
const provider = model.split("/")[0]
const rest = normalized.slice(start + marker.length)
const next = rest.search(new RegExp(`^${escapeRegExp(provider)}/`, "m"))
const metadata: unknown = JSON.parse((next < 0 ? rest : rest.slice(0, next)).trim())
if (!isRecord(metadata) || !isRecord(metadata.variants)) throw new Error(`Model variants not found: ${model}`)
return metadata.variants
}
export function translationConfig(agent: string, model: string, targets: string[]) {
return {
$schema: "https://opencode.ai/config.json",
model,
default_agent: agent,
share: "disabled" as const,
formatter: false,
lsp: false,
snapshot: false,
agent: {
[agent]: {
mode: "primary" as const,
model,
permission: {
"*": "deny" as const,
read: "allow" as const,
glob: "allow" as const,
grep: "allow" as const,
edit: Object.fromEntries([["*", "deny"], ...targets.map((target) => [target, "allow"])]),
},
},
},
}
}
export function unexpectedChanges(before: Record<string, string>, after: Record<string, string>, allowed: string[]) {
const targets = new Set(allowed)
return [...new Set([...Object.keys(before), ...Object.keys(after)])]
.filter((file) => !targets.has(file) && before[file] !== after[file])
.sort()
}
export async function runPool<T, R>(items: readonly T[], concurrency: number, task: (item: T) => Promise<R>) {
const results = new Map<number, R>()
const entries = items.entries()
const worker = async (): Promise<void> => {
const next = entries.next()
if (next.done) return
results.set(next.value[0], await task(next.value[1]))
await worker()
}
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker))
return Array.from(results.entries())
.sort((a, b) => a[0] - b[0])
.map((entry) => entry[1])
}
async function main() {
const options = parseTranslationArgs(Bun.argv.slice(2))
if (options.help) {
console.log(`
Usage: bun run translate:app -- <locale|all> [options]
Synchronizes product app translations with the English app, UI, and desktop dictionaries.
Options:
-c, --concurrency <count> Maximum parallel OpenCode runs for 'all' (default: 4)
--model <provider/id> OpenCode model (default: opencode/gpt-5.5)
--variant <name> Model variant (default: xhigh)
--dry-run Report drift without running OpenCode
--check Exit nonzero when translation drift exists
-h, --help Show this help message
Examples:
bun run translate:app -- fr
bun run translate:app -- all --concurrency 4
`)
return
}
const selected = options.target === "all" ? locales : [options.target]
const plans = await Promise.all(selected.map((locale) => inspect(locale)))
plans.forEach(report)
const pending = plans.filter((plan) => plan.domains.some((domain) => changed(domain.drift)))
if (options.check) {
if (pending.length) process.exitCode = 1
return
}
if (options.dryRun || pending.length === 0) return
const targets = pending.flatMap((plan) => plan.domains.map((domain) => domain.target))
const baseline = await worktreeSnapshot()
const variant = await resolveModelVariant(options.model, options.variant)
console.log(`Resolved ${options.model} (${options.variant}): ${JSON.stringify(variant)}`)
const template = await commandTemplate()
const results = await runPool(pending, options.concurrency, (plan) =>
translate(plan, template, options.model, options.variant).catch((error) => ({
locale: plan.locale,
code: 1,
stdout: "",
stderr: error instanceof Error ? error.message : String(error),
})),
)
results.forEach((result) => {
if (result.stdout) process.stdout.write(`\n[${result.locale}]\n${result.stdout}`)
if (result.stderr) process.stderr.write(`\n[${result.locale}]\n${result.stderr}`)
})
const failed = results.filter((result) => result.code !== 0)
const checks = await runPool(pending, options.concurrency, (plan) => check(plan.locale))
const incomplete = checks.filter((result) => result.code !== 0)
const escaped = unexpectedChanges(baseline, await worktreeSnapshot(), targets)
incomplete.forEach((result) => {
if (result.stdout) process.stderr.write(`\n[${result.locale} verification]\n${result.stdout}`)
if (result.stderr) process.stderr.write(`\n[${result.locale} verification]\n${result.stderr}`)
})
if (failed.length === 0 && incomplete.length === 0 && escaped.length === 0) {
console.log(`\nTranslated ${pending.map((plan) => plan.locale).join(", ")}.`)
return
}
if (failed.length) console.error(`\nOpenCode failed for: ${failed.map((result) => result.locale).join(", ")}`)
if (incomplete.length)
console.error(`Translation remains incomplete for: ${incomplete.map((plan) => plan.locale).join(", ")}`)
if (escaped.length) console.error(`Translation changed files outside its locale targets: ${escaped.join(", ")}`)
process.exitCode = 1
}
async function worktreeSnapshot() {
const groups = await Promise.all([
gitPaths(["diff", "--name-only", "-z", "HEAD"]),
gitPaths(["ls-files", "--others", "--exclude-standard", "-z"]),
])
const files = [...new Set(groups.flat())]
return Object.fromEntries(
await Promise.all(
files.map(async (file) => {
const target = Bun.file(path.join(root, file))
if (!(await target.exists())) return [file, "<missing>"] as const
const hash = new Bun.CryptoHasher("sha256")
hash.update(await target.arrayBuffer())
return [file, hash.digest("hex")] as const
}),
),
)
}
async function gitPaths(args: string[]) {
const proc = Bun.spawn(["git", ...args], {
cwd: root,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const result = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited])
if (result[2] !== 0) throw new Error(result[1] || `git ${args.join(" ")} failed`)
return result[0].split("\0").filter(Boolean)
}
async function check(locale: Locale) {
const proc = Bun.spawn([process.execPath, import.meta.path, locale, "--check"], {
cwd: root,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const result = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited])
return { locale, stdout: result[0], stderr: result[1], code: result[2] }
}
async function inspect(locale: Locale) {
const domains = await Promise.all(
targetFiles(locale).map(async (target) => {
const source = target.replace(`/${locale}.ts`, "/en.ts")
const dictionaries = await Promise.all([dictionary(source), dictionary(target)])
return {
name: target.includes("packages/app/") ? "app" : target.includes("packages/ui/") ? "ui" : "desktop",
source,
target,
drift: findDrift(dictionaries[0], dictionaries[1]),
}
}),
)
return { locale, language: languages[locale], domains }
}
async function dictionary(file: string) {
const module: unknown = await import(pathToFileURL(path.join(root, file)).href)
if (typeof module !== "object" || module === null || !("dict" in module) || !isDictionary(module.dict)) {
throw new Error(`Invalid translation dictionary: ${file}`)
}
return module.dict
}
async function commandTemplate() {
return (await Bun.file(path.join(root, "script/translate-app.md")).text()).trim()
}
async function translate(
plan: { locale: Locale; language: string; domains: Domain[] },
template: string,
model: string,
variant: string,
) {
const glossary = glossaryFile(plan.locale)
const glossaryContent = (await Bun.file(path.join(root, glossary)).exists())
? await Bun.file(path.join(root, glossary)).text()
: undefined
const prompt = template.replaceAll("$1", plan.locale).replaceAll(
"$ARGUMENTS",
JSON.stringify(
{
locale: plan.locale,
language: plan.language,
glossary: glossaryContent ? { file: glossary, content: glossaryContent } : undefined,
domains: plan.domains.map((domain) => ({
source: domain.source,
target: domain.target,
...domain.drift,
})),
},
null,
2,
),
)
const agent = `translate-app-${plan.locale}-${process.pid}`
const env = isolatedEnvironment()
env.KILO_DISABLE_PROJECT_CONFIG = "1"
env.KILO_CONFIG_CONTENT = JSON.stringify(
translationConfig(
agent,
model,
plan.domains.map((domain) => domain.target),
),
)
const proc = Bun.spawn(
[
"opencode",
"--pure",
"run",
"--dir",
root,
"--agent",
agent,
"--model",
model,
"--variant",
variant,
"--title",
`Translate app ${plan.locale}`,
"--format",
"json",
],
{
cwd: root,
env,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
},
)
const stdout = new Response(proc.stdout).text()
const stderr = new Response(proc.stderr).text()
await proc.stdin.write(prompt)
await proc.stdin.end()
const result = await Promise.all([stdout, stderr, proc.exited])
if (result[2] !== 0) return { locale: plan.locale, stdout: result[0], stderr: result[1], code: result[2] }
const sessionID = sessionIDFromEvents(result[0])
const exported = Bun.spawn(["opencode", "--pure", "export", sessionID, "--sanitize"], {
cwd: root,
env,
stdout: "pipe",
stderr: "pipe",
})
const exportResult = await Promise.all([
new Response(exported.stdout).text(),
new Response(exported.stderr).text(),
exported.exited,
])
if (exportResult[2] !== 0) {
return { locale: plan.locale, stdout: textFromEvents(result[0]), stderr: exportResult[1], code: exportResult[2] }
}
const session: unknown = JSON.parse(exportResult[0])
const observed = sessionModels(session)
const mismatch = observed.length === 0 || observed.some((item) => item.model !== model || item.variant !== variant)
const actual = Array.from(new Set(observed.map((item) => `${item.model} (${item.variant ?? "default"})`))).join(", ")
return {
locale: plan.locale,
stdout: `${textFromEvents(result[0])}\nVerified session model: ${actual}\n`,
stderr: mismatch
? `Requested ${model} (${variant}), but session used ${actual || "no assistant model"}.\n`
: result[1],
code: mismatch ? 1 : 0,
}
}
function report(plan: { locale: Locale; domains: Domain[] }) {
const details = plan.domains
.map(
(domain) =>
`${domain.name}: ${domain.drift.missing.length} missing, ${domain.drift.extra.length} extra, ${domain.drift.placeholders.length} placeholder mismatches`,
)
.join("; ")
console.log(`[${plan.locale}] ${details}`)
}
function changed(drift: Drift) {
return drift.missing.length > 0 || drift.extra.length > 0 || drift.placeholders.length > 0
}
function isLocale(value: string): value is Locale {
return Object.hasOwn(languages, value)
}
function isDictionary(value: unknown): value is Dictionary {
if (typeof value !== "object" || value === null || Array.isArray(value)) return false
return Object.values(value).every((item) => typeof item === "string")
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
async function resolveModelVariant(model: string, variant: string) {
const provider = model.split("/")[0]
if (!provider || !model.includes("/")) throw new Error(`Model must use provider/model syntax: ${model}`)
const env = isolatedEnvironment()
env.KILO_DISABLE_PROJECT_CONFIG = "1"
const proc = Bun.spawn(["opencode", "--pure", "models", provider, "--verbose"], {
cwd: root,
env,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const result = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited])
if (result[2] !== 0) throw new Error(result[1] || `Unable to resolve model: ${model}`)
const variants = modelVariants(result[0], model)
if (!Object.hasOwn(variants, variant)) throw new Error(`Variant '${variant}' is not configured for ${model}.`)
return variants[variant]
}
function isolatedEnvironment() {
const env = { ...process.env }
delete env.KILO_CONFIG
delete env.KILO_CONFIG_DIR
delete env.KILO_CONFIG_CONTENT
delete env.KILO_PERMISSION
delete env.KILO_AUTO_SHARE
return env
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
export function textFromEvents(output: string) {
return output
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.startsWith("{") && line.endsWith("}"))
.flatMap((line) => {
const event: unknown = JSON.parse(line)
if (!isRecord(event) || event.type !== "text" || !isRecord(event.part) || typeof event.part.text !== "string") {
return []
}
return [event.part.text.trim()]
})
.filter(Boolean)
.join("\n")
}
function tokens(value: string) {
return Array.from(value.matchAll(/{{\s*([^}]+?)\s*}}/g), (match) => match[1] ?? "").sort()
}
if (import.meta.main) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
}
@@ -1,5 +1,6 @@
import { expect, test } from "bun:test"
import { shouldSkip } from "./skip-files"
import { defaultConfig } from "../utils/config"
test("matches hosted package glob paths", () => {
expect(shouldSkip("packages/web/package.json", ["packages/web/**"])).toBe(true)
@@ -21,6 +22,16 @@ test("matches upstream stats package glob paths", () => {
expect(shouldSkip("packages/stats/core/src/index.ts", ["packages/stats/**"])).toBe(true)
})
test("matches upstream-only translation automation", () => {
expect(shouldSkip("script/translate-app.ts", defaultConfig.skipFiles)).toBe(true)
expect(shouldSkip("script/translate-app.test.ts", defaultConfig.skipFiles)).toBe(true)
expect(shouldSkip("script/translate-app.md", defaultConfig.skipFiles)).toBe(true)
})
test("transforms the Muse Spark prompt for Kilo branding", () => {
expect(defaultConfig.takeTheirsAndTransform).toContain("packages/opencode/src/session/prompt/meta.txt")
})
test("matches removed vscode sdk glob paths", () => {
expect(shouldSkip("sdks/vscode/package.json", ["sdks/vscode/**"])).toBe(true)
expect(shouldSkip("sdks/vscode/src/extension.ts", ["sdks/vscode/**"])).toBe(true)
@@ -0,0 +1,12 @@
import { expect, test } from "bun:test"
import { transformI18nContent } from "./transform-i18n"
test("marks transformed Kilo branding and preserves legacy config names", () => {
const result = transformI18nContent(
' "product": "OpenCode",\n "docs": "https://opencode.ai/docs",\n "legacy": ".opencode/opencode.json",',
)
expect(result.result).toContain('"product": "Kilo", // kilocode_change')
expect(result.result).toContain('"docs": "https://kilo.ai/docs", // kilocode_change')
expect(result.result).toContain('"legacy": ".opencode/opencode.json",')
expect(result.replacements).toBe(2)
})
+2 -1
View File
@@ -200,7 +200,8 @@ export function transformI18nContent(
}
}
transformedLines.push(transformedLine)
// Kilo branding produced by this transform remains a Kilo-owned delta in shared locale files.
transformedLines.push(lineReplacements > 0 ? `${transformedLine} // kilocode_change` : transformedLine)
totalReplacements += lineReplacements
}
@@ -69,6 +69,7 @@ test("fixScripts removes upstream-only dead scripts from root", () => {
"dev:desktop": "bun --cwd packages/desktop-electron dev",
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"translate:app": "bun run script/translate-app.ts",
},
}
const changes: string[] = []
@@ -78,7 +79,8 @@ test("fixScripts removes upstream-only dead scripts from root", () => {
expect(scripts["dev:desktop"]).toBeUndefined()
expect(scripts["dev:web"]).toBeUndefined()
expect(scripts["dev:console"]).toBeUndefined()
expect(changes.length).toBe(3)
expect(scripts["translate:app"]).toBeUndefined()
expect(changes.length).toBe(4)
})
test("fixScripts preserves opencode test scripts", () => {
@@ -320,7 +320,7 @@ const DELETE_UPSTREAM_TRUSTED_DEPS: Record<string, string[]> = {
// Kilo doesn't ship (desktop-electron, console/app, app) and would otherwise
// reappear on every merge.
const DELETE_UPSTREAM_SCRIPTS: Record<string, string[]> = {
"package.json": ["dev:desktop", "dev:web", "dev:console"],
"package.json": ["dev:desktop", "dev:web", "dev:console", "translate:app"],
}
// Upstream-only catalog entries to delete per package.json. These are pulled
+6
View File
@@ -137,6 +137,10 @@ export const defaultConfig: MergeConfig = {
"packages/opencode/bin/opencode",
// Removed prompt file
"packages/opencode/src/session/prompt/build-switch.txt",
// Upstream app translation automation targets products and binaries Kilo does not ship
"script/translate-app.ts",
"script/translate-app.test.ts",
"script/translate-app.md",
// Vouch files (Kilo doesn't use Vouch).
// Upstream currently ships VOUCHED.td (typo extension). The glob covers both
// the current .td file and any future .md rename without another merge breaking.
@@ -174,6 +178,8 @@ export const defaultConfig: MergeConfig = {
// Files that should take upstream version and apply Kilo branding transforms
// These are files with only branding differences, no logic changes
takeTheirsAndTransform: [
// Model-facing prompts that need Kilo product identity and documentation links
"packages/opencode/src/session/prompt/meta.txt",
// UI components
"packages/ui/src/components/**/*.tsx",
"packages/ui/src/context/**/*.tsx",