diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9a888adbb..3aeef82d6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,5 @@ # web + desktop packages packages/app/ @adamdotdevin packages/tauri/ @adamdotdevin +packages/desktop/src-tauri/ @brendonovich packages/desktop/ @adamdotdevin diff --git a/bun.lock b/bun.lock index d830e9b4c..06b7db1d2 100644 --- a/bun.lock +++ b/bun.lock @@ -323,6 +323,9 @@ "packages/kilo-vscode": { "name": "kilo-code", "version": "0.0.1", + "dependencies": { + "solid-js": "^1.9.11", + }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", @@ -330,6 +333,7 @@ "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "esbuild": "^0.27.2", + "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "npm-run-all": "^4.1.5", "typescript": "^5.9.3", @@ -2590,6 +2594,8 @@ "esbuild-plugin-copy": ["esbuild-plugin-copy@2.1.1", "", { "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", "fs-extra": "^10.0.1", "globby": "^11.0.3" }, "peerDependencies": { "esbuild": ">= 0.14.0" } }, "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw=="], + "esbuild-plugin-solid": ["esbuild-plugin-solid@0.6.0", "", { "dependencies": { "@babel/core": "^7.20.12", "@babel/preset-typescript": "^7.18.6", "babel-preset-solid": "^1.6.9" }, "peerDependencies": { "esbuild": ">=0.20", "solid-js": ">= 1.0" } }, "sha512-V1FvDALwLDX6K0XNYM9CMRAnMzA0+Ecu55qBUT9q/eAJh1KIDsTMFoOzMSgyHqbOfvrVfO3Mws3z7TW2GVnIZA=="], + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -4736,6 +4742,8 @@ "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "kilo-code/solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], + "kilo-code/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -5476,6 +5484,10 @@ "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "kilo-code/solid-js/seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], + + "kilo-code/solid-js/seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index a98461c37..3e6310d88 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1771,7 +1771,7 @@ export default function Page() {
{ const os = platform() @@ -146,12 +153,7 @@ export namespace Clipboard { }) export async function copy(text: string): Promise { - const renderer = rendererRef.current - if (renderer) { - // Try OSC52 but don't early return - always fall back to native method - // OSC52 may report success but not actually work in all terminals - renderer.copyToClipboardOSC52(text) - } + writeOsc52(text) await getCopyMethod()(text) } } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ed24584fc..2990bfca3 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -221,7 +221,9 @@ export namespace Provider { options: providerOptions, async getModel(sdk: any, modelID: string, options?: Record) { // Skip region prefixing if model already has a cross-region inference profile prefix - if (modelID.startsWith("global.") || modelID.startsWith("jp.")) { + // Models from models.dev may already include prefixes like us., eu., global., etc. + const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] + if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) { return sdk.languageModel(modelID) } @@ -1001,21 +1003,6 @@ export namespace Provider { const fetchFn = customFetch ?? fetch const opts = init ?? {} - // Merge configured headers into request headers - // kilocode_change start - Handle Headers instances correctly (spreading Headers gives {}) - const existingHeaders = - opts.headers instanceof Headers - ? Object.fromEntries(opts.headers.entries()) - : typeof opts.headers === "object" - ? opts.headers - : {} - - opts.headers = { - ...existingHeaders, - ...options["headers"], - } - // kilocode_change end - if (options["timeout"] !== undefined && options["timeout"] !== null) { const signals: AbortSignal[] = [] if (opts.signal) signals.push(opts.signal) diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 65ca1e9bb..6fb2a7aeb 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -17,13 +17,14 @@ const FILES = [ ] function globalFiles() { - const files = [path.join(Global.Path.config, "AGENTS.md")] - if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) { - files.push(path.join(os.homedir(), ".claude", "CLAUDE.md")) - } + const files = [] if (Flag.OPENCODE_CONFIG_DIR) { files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md")) } + files.push(path.join(Global.Path.config, "AGENTS.md")) + if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) { + files.push(path.join(os.homedir(), ".claude", "CLAUDE.md")) + } return files } diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 6b5cf681c..a90c9632d 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -1,4 +1,4 @@ -import { test, expect, mock } from "bun:test" +import { test, expect, mock, describe } from "bun:test" import path from "path" import { unlink } from "fs/promises" @@ -266,3 +266,214 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () }, }) }) + +// Tests for cross-region inference profile prefix handling +// Models from models.dev may come with prefixes already (e.g., us., eu., global.) +// These should NOT be double-prefixed when passed to the SDK + +test("Bedrock: model with us. prefix should not be double-prefixed", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "us-east-1", + }, + models: { + "us.anthropic.claude-opus-4-5-20251101-v1:0": { + name: "Claude Opus 4.5 (US)", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + // The model should exist with the us. prefix + expect(providers["amazon-bedrock"].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + }, + }) +}) + +test("Bedrock: model with global. prefix should not be prefixed", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "us-east-1", + }, + models: { + "global.anthropic.claude-opus-4-5-20251101-v1:0": { + name: "Claude Opus 4.5 (Global)", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + }, + }) +}) + +test("Bedrock: model with eu. prefix should not be double-prefixed", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "eu-west-1", + }, + models: { + "eu.anthropic.claude-opus-4-5-20251101-v1:0": { + name: "Claude Opus 4.5 (EU)", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + }, + }) +}) + +test("Bedrock: model without prefix in US region should get us. prefix added", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "us-east-1", + }, + models: { + "anthropic.claude-opus-4-5-20251101-v1:0": { + name: "Claude Opus 4.5", + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + // Non-prefixed model should still be registered + expect(providers["amazon-bedrock"].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined() + }, + }) +}) + +// Direct unit tests for cross-region inference profile prefix handling +// These test the prefix detection logic used in getModel + +describe("Bedrock cross-region prefix detection", () => { + const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] + + test("should detect global. prefix", () => { + const modelID = "global.anthropic.claude-opus-4-5-20251101-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(true) + }) + + test("should detect us. prefix", () => { + const modelID = "us.anthropic.claude-opus-4-5-20251101-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(true) + }) + + test("should detect eu. prefix", () => { + const modelID = "eu.anthropic.claude-opus-4-5-20251101-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(true) + }) + + test("should detect jp. prefix", () => { + const modelID = "jp.anthropic.claude-sonnet-4-20250514-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(true) + }) + + test("should detect apac. prefix", () => { + const modelID = "apac.anthropic.claude-sonnet-4-20250514-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(true) + }) + + test("should detect au. prefix", () => { + const modelID = "au.anthropic.claude-sonnet-4-5-20250929-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(true) + }) + + test("should NOT detect prefix for non-prefixed model", () => { + const modelID = "anthropic.claude-opus-4-5-20251101-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(false) + }) + + test("should NOT detect prefix for amazon nova models", () => { + const modelID = "amazon.nova-pro-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(false) + }) + + test("should NOT detect prefix for cohere models", () => { + const modelID = "cohere.command-r-plus-v1:0" + const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix)) + expect(hasPrefix).toBe(false) + }) +}) diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index 4d57e92a2..e0bf94a95 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, test } from "bun:test" +import { afterEach, beforeEach, describe, expect, test } from "bun:test" import path from "path" import { InstructionPrompt } from "../../src/session/instruction" import { Instance } from "../../src/project/instance" +import { Global } from "../../src/global" import { tmpdir } from "../fixture/fixture" describe("InstructionPrompt.resolve", () => { @@ -68,3 +69,102 @@ describe("InstructionPrompt.resolve", () => { }) }) }) + +describe("InstructionPrompt.systemPaths OPENCODE_CONFIG_DIR", () => { + let originalConfigDir: string | undefined + + beforeEach(() => { + originalConfigDir = process.env["OPENCODE_CONFIG_DIR"] + }) + + afterEach(() => { + if (originalConfigDir === undefined) { + delete process.env["OPENCODE_CONFIG_DIR"] + } else { + process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir + } + }) + + test("prefers OPENCODE_CONFIG_DIR AGENTS.md over global when both exist", async () => { + await using profileTmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "AGENTS.md"), "# Profile Instructions") + }, + }) + await using globalTmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions") + }, + }) + await using projectTmp = await tmpdir() + + process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path + const originalGlobalConfig = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + + try { + await Instance.provide({ + directory: projectTmp.path, + fn: async () => { + const paths = await InstructionPrompt.systemPaths() + expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(true) + expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(false) + }, + }) + } finally { + ;(Global.Path as { config: string }).config = originalGlobalConfig + } + }) + + test("falls back to global AGENTS.md when OPENCODE_CONFIG_DIR has no AGENTS.md", async () => { + await using profileTmp = await tmpdir() + await using globalTmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions") + }, + }) + await using projectTmp = await tmpdir() + + process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path + const originalGlobalConfig = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + + try { + await Instance.provide({ + directory: projectTmp.path, + fn: async () => { + const paths = await InstructionPrompt.systemPaths() + expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(false) + expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true) + }, + }) + } finally { + ;(Global.Path as { config: string }).config = originalGlobalConfig + } + }) + + test("uses global AGENTS.md when OPENCODE_CONFIG_DIR is not set", async () => { + await using globalTmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions") + }, + }) + await using projectTmp = await tmpdir() + + delete process.env["OPENCODE_CONFIG_DIR"] + const originalGlobalConfig = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + + try { + await Instance.provide({ + directory: projectTmp.path, + fn: async () => { + const paths = await InstructionPrompt.systemPaths() + expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true) + }, + }) + } finally { + ;(Global.Path as { config: string }).config = originalGlobalConfig + } + }) +}) diff --git a/packages/script/src/index.ts b/packages/script/src/index.ts index 675d5b1bf..d4e1826eb 100644 --- a/packages/script/src/index.ts +++ b/packages/script/src/index.ts @@ -70,8 +70,8 @@ export const Script = { get preview() { return IS_PREVIEW }, - get release() { - return env.OPENCODE_RELEASE + get release(): boolean { + return !!env.OPENCODE_RELEASE }, get team() { return team diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 8589bd552..fcadd6b62 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -46,7 +46,7 @@ Kilo comes with two built-in primary agents and two built-in subagents. --- -### Build +### Use build _Mode_: `primary` @@ -54,7 +54,7 @@ Build is the **default** primary agent with all tools enabled. This is the stand --- -### Plan +### Use plan _Mode_: `primary` @@ -68,7 +68,7 @@ This agent is useful when you want the LLM to analyze code, suggest changes, or --- -### General +### Use general _Mode_: `subagent` @@ -76,7 +76,7 @@ A general-purpose agent for researching complex questions and executing multi-st --- -### Explore +### Use explore _Mode_: `subagent` @@ -84,6 +84,30 @@ A fast, read-only agent for exploring codebases. Cannot modify files. Use this w --- +### Use compaction + +_Mode_: `primary` + +Hidden system agent that compacts long context into a smaller summary. It runs automatically when needed and is not selectable in the UI. + +--- + +### Use title + +_Mode_: `primary` + +Hidden system agent that generates short session titles. It runs automatically and is not selectable in the UI. + +--- + +### Use summary + +_Mode_: `primary` + +Hidden system agent that creates session summaries. It runs automatically and is not selectable in the UI. + +--- + ## Usage 1. For primary agents, use the **Tab** key to cycle through them during a session. You can also use your configured `switch_agent` keybind. diff --git a/packages/web/src/content/docs/skills.mdx b/packages/web/src/content/docs/skills.mdx index 70a6ca288..163b8ec20 100644 --- a/packages/web/src/content/docs/skills.mdx +++ b/packages/web/src/content/docs/skills.mdx @@ -17,15 +17,17 @@ Kilo searches these locations: - Global config: `~/.config/opencode/skills//SKILL.md` - Project Claude-compatible: `.claude/skills//SKILL.md` - Global Claude-compatible: `~/.claude/skills//SKILL.md` +- Project agent-compatible: `.agents/skills//SKILL.md` +- Global agent-compatible: `~/.agents/skills//SKILL.md` --- ## Understand discovery For project-local paths, Kilo walks up from your current working directory until it reaches the git worktree. -It loads any matching `skills/*/SKILL.md` in `.opencode/` and any matching `.claude/skills/*/SKILL.md` along the way. +It loads any matching `skills/*/SKILL.md` in `.opencode/` and any matching `.claude/skills/*/SKILL.md` or `.agents/skills/*/SKILL.md` along the way. -Global definitions are also loaded from `~/.config/opencode/skills/*/SKILL.md` and `~/.claude/skills/*/SKILL.md`. +Global definitions are also loaded from `~/.config/opencode/skills/*/SKILL.md`, `~/.claude/skills/*/SKILL.md`, and `~/.agents/skills/*/SKILL.md`. --- diff --git a/script/version.ts b/script/version.ts index 1b7eac5d9..e011f4453 100755 --- a/script/version.ts +++ b/script/version.ts @@ -14,8 +14,8 @@ if (!Script.preview) { const file = `${dir}/opencode-release-notes.txt` await Bun.write(file, body) await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes-file ${file}` - const release = await $`gh release view v${Script.version} --json id,tagName`.json() - output.push(`release=${release.id}`) + const release = await $`gh release view v${Script.version} --json tagName,databaseId`.json() + output.push(`release=${release.databaseId}`) output.push(`tag=${release.tagName}`) }