diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 4c40a0fa6a..616dcc0911 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "1.3.5" +version = "1.3.6" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.5/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.6/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.5/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.6/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.5/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.6/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.5/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.6/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.5/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v1.3.6/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md index 3eae937050..f1e6c81fd5 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md @@ -165,26 +165,25 @@ Positionals: message message to send [string] [default: []] Options: - --help Show help [boolean] - --version Show version number [boolean] - --command the command to run, use message for args [string] - -c, --continue continue the last session [boolean] - -s, --session session id to continue [string] - --fork fork the session before continuing (requires --continue or --session) [boolean] - --cloud-fork fetch session from cloud and continue locally (requires --session) [boolean] - --share share the session [boolean] - -m, --model model to use in the format of provider/model [string] - --agent agent to use [string] - --format format: default (formatted) or json (raw JSON events) [string] [choices: "default", "json"] [default: "default"] - -f, --file file(s) to attach to message [array] - --title title for the session (uses truncated prompt if no value provided) [string] - --attach attach to a running opencode server (e.g., http://localhost:4096) [string] - -p, --password basic auth password (defaults to KILO_SERVER_PASSWORD) [string] - --dir directory to run in, path on remote server if attaching [string] - --port port for the local server (defaults to random port if no value provided) [number] - --variant model variant (provider-specific reasoning effort, e.g., high, max, minimal) [string] - --thinking show thinking blocks [boolean] [default: false] - --auto auto-approve all permissions (for autonomous/pipeline usage) [boolean] [default: false] + --help Show help [boolean] + --version Show version number [boolean] + --command the command to run, use message for args [string] + -c, --continue continue the last session [boolean] + -s, --session session id to continue [string] + --fork fork the session before continuing (requires --continue or --session) [boolean] + --share share the session [boolean] + -m, --model model to use in the format of provider/model [string] + --agent agent to use [string] + --format format: default (formatted) or json (raw JSON events) [string] [choices: "default", "json"] [default: "default"] + --auto auto-approve all permissions (for autonomous/pipeline usage) [boolean] [default: false] + -f, --file file(s) to attach to message [array] + --title title for the session (uses truncated prompt if no value provided) [string] + --attach attach to a running opencode server (e.g., http://localhost:4096) [string] + -p, --password basic auth password (defaults to KILO_SERVER_PASSWORD) [string] + --dir directory to run in, path on remote server if attaching [string] + --port port for the local server (defaults to random port if no value provided) [number] + --variant model variant (provider-specific reasoning effort, e.g., high, max, minimal) [string] + --thinking show thinking blocks [boolean] [default: false] ``` ## kilo debug diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-util.ts b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-util.ts index d6359eafd9..b8b284d834 100644 --- a/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-util.ts +++ b/packages/kilo-vscode/src/legacy-migration/sessions/lib/parts/parts-util.ts @@ -55,17 +55,17 @@ export function isCompletionResult( ): input is { type?: string; name?: string; input: { result: string } } { return Boolean( input && - typeof input === "object" && - "type" in input && - input.type === "tool_use" && - "name" in input && - input.name === "attempt_completion" && - "input" in input && - input.input && - typeof input.input === "object" && - "result" in input.input && - typeof input.input.result === "string" && - input.input.result, + typeof input === "object" && + "type" in input && + input.type === "tool_use" && + "name" in input && + input.name === "attempt_completion" && + "input" in input && + input.input && + typeof input.input === "object" && + "result" in input.input && + typeof input.input.result === "string" && + input.input.result, ) } diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/PopupSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/PopupSelector.tsx index 27426e3924..207e2f7aaf 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/PopupSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/PopupSelector.tsx @@ -22,10 +22,8 @@ import { import { Popover } from "@kilocode/kilo-ui/popover" import type { PopoverProps } from "@kilocode/kilo-ui/popover" -export interface PopupSelectorProps extends Omit< - PopoverProps, - "style" | "children" -> { +export interface PopupSelectorProps + extends Omit, "style" | "children"> { /** Whether the selector is in expanded mode (wider + taller). */ expanded: boolean /** Preferred width when collapsed. Default: 250 */ diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index e2bd754016..6a124a4f3a 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -250,10 +250,6 @@ export const RunCommand = cmd({ describe: "fork the session before continuing (requires --continue or --session)", type: "boolean", }) - .option("cloud-fork", { - describe: "fetch session from cloud and continue locally (requires --session)", - type: "boolean", - }) .option("share", { type: "boolean", describe: "share the session", @@ -273,6 +269,13 @@ export const RunCommand = cmd({ default: "default", describe: "format: default (formatted) or json (raw JSON events)", }) + // kilocode_change start - auto approve all permissions + .option("auto", { + type: "boolean", + describe: "auto-approve all permissions (for autonomous/pipeline usage)", + default: false, + }) + // kilocode_change end .option("file", { alias: ["f"], type: "string", @@ -287,13 +290,11 @@ export const RunCommand = cmd({ type: "string", describe: "attach to a running opencode server (e.g., http://localhost:4096)", }) - // kilocode_change start .option("password", { alias: ["p"], type: "string", describe: "basic auth password (defaults to KILO_SERVER_PASSWORD)", }) - // kilocode_change end .option("dir", { type: "string", describe: "directory to run in, path on remote server if attaching", @@ -311,13 +312,6 @@ export const RunCommand = cmd({ describe: "show thinking blocks", default: false, }) - // kilocode_change start - auto approve all permissions - .option("auto", { - type: "boolean", - describe: "auto-approve all permissions (for autonomous/pipeline usage)", - default: false, - }) - // kilocode_change end ) }, handler: async (args) => { diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 17e78c7df8..b34e9c00e1 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -1,7 +1,7 @@ import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { Clipboard } from "@tui/util/clipboard" import { Selection } from "@tui/util/selection" -import { createCliRenderer, MouseButton, TextAttributes, type CliRendererConfig } from "@opentui/core" +import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx index fd895e0cf6..872092d23e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx @@ -24,7 +24,6 @@ export function DialogVariant() { title={"Select variant"} current={local.model.variant.current()} flat={true} - skipFilter={true} /> ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx deleted file mode 100644 index dbfb747a9b..0000000000 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ /dev/null @@ -1,173 +0,0 @@ -// kilocode_change - retained; upstream moved session header into prompt footer in #19486, Kilo keeps the dedicated header UI -import { type Accessor, createMemo, createSignal, Match, Show, Switch } from "solid-js" -import { useRouteData } from "@tui/context/route" -import { useSync } from "@tui/context/sync" -import { pipe, sumBy } from "remeda" -import { useTheme } from "@tui/context/theme" -import { SplitBorder } from "@tui/component/border" -import type { AssistantMessage, Session } from "@kilocode/sdk/v2" -import { useCommandDialog } from "@tui/component/dialog-command" -import { useKeybind } from "../../context/keybind" -import { Flag } from "@/flag/flag" -import { useTerminalDimensions } from "@opentui/solid" - -const Title = (props: { session: Accessor }) => { - const { theme } = useTheme() - return ( - - # {props.session().title} - - ) -} - -const ContextInfo = (props: { context: Accessor; cost: Accessor }) => { - const { theme } = useTheme() - return ( - - - {props.context()} ({props.cost()}) - - - ) -} - -const WorkspaceInfo = (props: { workspace: Accessor }) => { - const { theme } = useTheme() - return ( - - - {props.workspace()} - - - ) -} - -export function Header() { - const route = useRouteData("session") - const sync = useSync() - const session = createMemo(() => sync.session.get(route.sessionID)!) - const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) - - const cost = createMemo(() => { - const total = pipe( - messages(), - sumBy((x) => (x.role === "assistant" ? x.cost : 0)), - ) - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(total) - }) - - const context = createMemo(() => { - const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage - if (!last) return - const total = - last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write - const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] - let result = total.toLocaleString() - if (model?.limit.context) { - result += " " + Math.round((total / model.limit.context) * 100) + "%" - } - return result - }) - - const workspace = createMemo(() => { - const id = session()?.workspaceID - if (!id) return "Workspace local" - const info = sync.workspace.get(id) - if (!info) return `Workspace ${id}` - return `Workspace ${id} (${info.type})` - }) - - const { theme } = useTheme() - const keybind = useKeybind() - const command = useCommandDialog() - const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null) - const dimensions = useTerminalDimensions() - const narrow = createMemo(() => dimensions().width < 80) - - return ( - - - - - - - {Flag.KILO_EXPERIMENTAL_WORKSPACES ? ( - - - Subagent session - - - - ) : ( - - Subagent session - - )} - - - - - setHover("parent")} - onMouseOut={() => setHover(null)} - onMouseUp={() => command.trigger("session.parent")} - backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel} - > - - Parent {keybind.print("session_parent")} - - - setHover("prev")} - onMouseOut={() => setHover(null)} - onMouseUp={() => command.trigger("session.child.previous")} - backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel} - > - - Prev {keybind.print("session_child_cycle_reverse")} - - - setHover("next")} - onMouseOut={() => setHover(null)} - onMouseUp={() => command.trigger("session.child.next")} - backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel} - > - - Next {keybind.print("session_child_cycle")} - - - - - - - - {Flag.KILO_EXPERIMENTAL_WORKSPACES ? ( - - - <WorkspaceInfo workspace={workspace} /> - </box> - ) : ( - <Title session={session} /> - )} - <ContextInfo context={context} cost={cost} /> - </box> - </Match> - </Switch> - </box> - </box> - ) -} diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 72b4d12358..61d5d15630 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -21,7 +21,7 @@ import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" import { writeHeapSnapshot } from "v8" declare global { - const KILO_WORKER_PATH: string // kilocode_change + const KILO_WORKER_PATH: string } type RpcClient = ReturnType<typeof Rpc.client<typeof rpc>> diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 005cc21b8e..43036c55b5 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1852,6 +1852,10 @@ export namespace Config { return runPromise((svc) => svc.waitForDependencies()) } + export async function waitForDependencies() { + return runPromise((svc) => svc.waitForDependencies()) + } + // kilocode_change start export async function warnings() { return runPromise((svc) => svc.warnings()) diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index fc51577037..719748e3a1 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/packages/opencode/src/control-plane/adaptors/worktree.ts @@ -32,14 +32,7 @@ export const WorktreeAdaptor: Adaptor = { const config = Config.parse(info) await Worktree.remove({ directory: config.directory }) }, - async fetch(info, input: RequestInfo | URL, init?: RequestInit) { - const config = Config.parse(info) - const { WorkspaceServer } = await import("../workspace-server/server") - const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://kilo.internal") - const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)) - headers.set("x-kilo-directory", config.directory) - - const request = new Request(url, { ...init, headers }) - return WorkspaceServer.App().fetch(request) + async fetch(_info, _input: RequestInfo | URL, _init?: RequestInit) { + throw new Error("fetch not implemented") }, } diff --git a/packages/opencode/src/control-plane/workspace-context.ts b/packages/opencode/src/control-plane/workspace-context.ts deleted file mode 100644 index 11268605af..0000000000 --- a/packages/opencode/src/control-plane/workspace-context.ts +++ /dev/null @@ -1,25 +0,0 @@ -// kilocode_change - retained; upstream removed workspace-server subsystem in #19316, routing refactor incomplete in v1.3.5 -import { Context } from "../util/context" -import type { WorkspaceID } from "./schema" - -interface Context { - workspaceID?: WorkspaceID -} - -const context = Context.create<Context>("workspace") - -export const WorkspaceContext = { - async provide<R>(input: { workspaceID?: WorkspaceID; fn: () => R }): Promise<R> { - return context.provide({ workspaceID: input.workspaceID }, async () => { - return input.fn() - }) - }, - - get workspaceID() { - try { - return context.use().workspaceID - } catch (e) { - return undefined - } - }, -} diff --git a/packages/opencode/src/control-plane/workspace-router-middleware.ts b/packages/opencode/src/control-plane/workspace-router-middleware.ts deleted file mode 100644 index fd0a8d6a06..0000000000 --- a/packages/opencode/src/control-plane/workspace-router-middleware.ts +++ /dev/null @@ -1,51 +0,0 @@ -// kilocode_change - retained; upstream removed workspace-server subsystem in #19316, routing refactor incomplete in v1.3.5 -import type { MiddlewareHandler } from "hono" -import { Flag } from "../flag/flag" -import { getAdaptor } from "./adaptors" -import { Workspace } from "./workspace" -import { WorkspaceContext } from "./workspace-context" - -// This middleware forwards all non-GET requests if the workspace is a -// remote. The remote workspace needs to handle session mutations -async function routeRequest(req: Request) { - // Right now, we need to forward all requests to the workspace - // because we don't have syncing. In the future all GET requests - // which don't mutate anything will be handled locally - // - // if (req.method === "GET") return - - if (!WorkspaceContext.workspaceID) return - - const workspace = await Workspace.get(WorkspaceContext.workspaceID) - if (!workspace) { - return new Response(`Workspace not found: ${WorkspaceContext.workspaceID}`, { - status: 500, - headers: { - "content-type": "text/plain; charset=utf-8", - }, - }) - } - - const adaptor = await getAdaptor(workspace.type) - - return adaptor.fetch(workspace, `${new URL(req.url).pathname}${new URL(req.url).search}`, { - method: req.method, - body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(), - signal: req.signal, - headers: req.headers, - }) -} - -export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => { - // Only available in development for now - if (!Flag.KILO_EXPERIMENTAL_WORKSPACES) { - // kilocode_change - return next() - } - - const response = await routeRequest(c.req.raw) - if (response) { - return response - } - return next() -} diff --git a/packages/opencode/src/control-plane/workspace-server/routes.ts b/packages/opencode/src/control-plane/workspace-server/routes.ts deleted file mode 100644 index e13449f7a0..0000000000 --- a/packages/opencode/src/control-plane/workspace-server/routes.ts +++ /dev/null @@ -1,34 +0,0 @@ -// kilocode_change - retained; upstream removed workspace-server subsystem in #19316, routing refactor incomplete in v1.3.5 -import { GlobalBus } from "../../bus/global" -import { Hono } from "hono" -import { streamSSE } from "hono/streaming" - -export function WorkspaceServerRoutes() { - return new Hono().get("/event", async (c) => { - c.header("X-Accel-Buffering", "no") - c.header("X-Content-Type-Options", "nosniff") - return streamSSE(c, async (stream) => { - const send = async (event: unknown) => { - await stream.writeSSE({ - data: JSON.stringify(event), - }) - } - const handler = async (event: { directory?: string; payload: unknown }) => { - await send(event.payload) - } - GlobalBus.on("event", handler) - await send({ type: "server.connected", properties: {} }) - const heartbeat = setInterval(() => { - void send({ type: "server.heartbeat", properties: {} }) - }, 10_000) - - await new Promise<void>((resolve) => { - stream.onAbort(() => { - clearInterval(heartbeat) - GlobalBus.off("event", handler) - resolve() - }) - }) - }) - }) -} diff --git a/packages/opencode/src/control-plane/workspace-server/server.ts b/packages/opencode/src/control-plane/workspace-server/server.ts deleted file mode 100644 index 53836bd877..0000000000 --- a/packages/opencode/src/control-plane/workspace-server/server.ts +++ /dev/null @@ -1,66 +0,0 @@ -// kilocode_change - retained; upstream removed workspace-server subsystem in #19316, routing refactor incomplete in v1.3.5 -import { Hono } from "hono" -import { Instance } from "../../project/instance" -import { InstanceBootstrap } from "../../project/bootstrap" -import { SessionRoutes } from "../../server/routes/session" -import { WorkspaceServerRoutes } from "./routes" -import { WorkspaceContext } from "../workspace-context" -import { WorkspaceID } from "../schema" - -export namespace WorkspaceServer { - export function App() { - const session = new Hono() - .use(async (c, next) => { - // Right now, we need handle all requests because we don't - // have syncing. In the future all GET requests will handled - // by the control plane - // - // if (c.req.method === "GET") return c.notFound() - await next() - }) - .route("/", SessionRoutes()) - - return new Hono() - .use(async (c, next) => { - const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-kilo-workspace") - const raw = c.req.query("directory") || c.req.header("x-kilo-directory") - if (rawWorkspaceID == null) { - throw new Error("workspaceID parameter is required") - } - if (raw == null) { - throw new Error("directory parameter is required") - } - - const directory = (() => { - try { - return decodeURIComponent(raw) - } catch { - return raw - } - })() - - return WorkspaceContext.provide({ - workspaceID: WorkspaceID.make(rawWorkspaceID), - async fn() { - return Instance.provide({ - directory, - init: InstanceBootstrap, - async fn() { - return next() - }, - }) - }, - }) - }) - .route("/session", session) - .route("/", WorkspaceServerRoutes()) - } - - export function Listen(opts: { hostname: string; port: number }) { - return Bun.serve({ - hostname: opts.hostname, - port: opts.port, - fetch: App().fetch, - }) - } -} diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 431445d99a..8e5a082457 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -15,7 +15,7 @@ export namespace Global { export const Path = { // Allow override via KILO_TEST_HOME for test isolation get home() { - return process.env.KILO_TEST_HOME || os.homedir() // kilocode_change + return process.env.KILO_TEST_HOME || os.homedir() }, data, bin: path.join(cache, "bin"), diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index cec7d2fc42..04687faa38 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -98,7 +98,7 @@ let cli = yargs(hideBin(process.argv)) // kilocode_change }) .middleware(async (opts) => { if (opts.pure) { - process.env.KILO_PURE = "1" // kilocode_change + process.env.KILO_PURE = "1" } await Log.init({ diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 8b87200554..ad35ed5b55 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -61,8 +61,8 @@ export namespace Installation { }) export type Info = z.infer<typeof Info> - export const VERSION = typeof KILO_VERSION === "string" ? KILO_VERSION : "local" // kilocode_change - export const CHANNEL = typeof KILO_CHANNEL === "string" ? KILO_CHANNEL : "local" // kilocode_change + export const VERSION = typeof KILO_VERSION === "string" ? KILO_VERSION : "local" + export const CHANNEL = typeof KILO_CHANNEL === "string" ? KILO_CHANNEL : "local" export const USER_AGENT = `kilo/${CHANNEL}/${VERSION}/${Flag.KILO_CLIENT}` // kilocode_change export function isPreview() { diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 60e6eefc78..4bfd9a891e 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -260,9 +260,7 @@ export namespace Project { time: { created: Date.now(), updated: Date.now() }, } - // kilocode_change start if (Flag.KILO_EXPERIMENTAL_ICON_DISCOVERY) yield* discover(existing).pipe(Effect.ignore, Effect.forkIn(scope)) - // kilocode_change end const result: Info = { ...existing, diff --git a/packages/opencode/src/server/router.ts b/packages/opencode/src/server/router.ts index 70c3652447..a08d5bb85b 100644 --- a/packages/opencode/src/server/router.ts +++ b/packages/opencode/src/server/router.ts @@ -7,7 +7,6 @@ import { Filesystem } from "@/util/filesystem" import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { InstanceRoutes } from "./instance" -import { HEADER_DIRECTORY, HEADER_WORKSPACE } from "../kilocode/server/router" // kilocode_change type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } @@ -28,7 +27,7 @@ function local(method: string, path: string) { const routes = lazy(() => InstanceRoutes()) export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { - const raw = c.req.query("directory") || c.req.header(HEADER_DIRECTORY) || process.cwd() // kilocode_change + const raw = c.req.query("directory") || c.req.header("x-kilo-directory") || process.cwd() const directory = Filesystem.resolve( (() => { try { @@ -40,7 +39,7 @@ export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { ) const url = new URL(c.req.url) - const workspaceParam = url.searchParams.get("workspace") || c.req.header(HEADER_WORKSPACE) // kilocode_change + const workspaceParam = url.searchParams.get("workspace") // TODO: If session is being routed, force it to lookup the // project/workspace @@ -89,7 +88,7 @@ export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { const adaptor = await getAdaptor(workspace.type) const headers = new Headers(c.req.raw.headers) - headers.delete(HEADER_WORKSPACE) // kilocode_change + headers.delete("x-kilo-workspace") return adaptor.fetch(workspace, `${url.pathname}${url.search}`, { method: c.req.method, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 0403b2e617..07b70ae172 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -34,7 +34,6 @@ import { ModelID, ProviderID } from "@/provider/schema" import { Permission } from "@/permission" import { Global } from "@/global" import type { LanguageModelV2Usage } from "@ai-sdk/provider" -import { iife } from "@/util/iife" import { Effect, Layer, Scope, ServiceMap } from "effect" import { makeRuntime } from "@/effect/run-service" @@ -252,12 +251,6 @@ export namespace Session { // kilocode_change end } - // kilocode_change - export type CloseReason = KiloSession.CloseReason - - // kilocode_change - export const getPlatformOverride = KiloSession.getPlatformOverride - export function plan(input: { slug: string; time: { created: number } }) { const base = Instance.project.vcs ? path.join(Instance.worktree, ".kilo", "plans") // kilocode_change @@ -289,27 +282,12 @@ export namespace Session { 0) as number, ) - // OpenRouter provides inputTokens as the total count of input tokens (including cached). - // AFAIK other providers (OpenRouter/OpenAI/Gemini etc.) do it the same way e.g. vercel/ai#8794 (comment) - // Anthropic does it differently though - inputTokens doesn't include cached tokens. - // It looks like OpenCode's cost calculation assumes all providers return inputTokens the same way Anthropic does (I'm guessing getUsage logic was originally implemented with anthropic), so it's causing incorrect cost calculation for OpenRouter and others. - const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"]) - const adjustedInputTokens = safe( - excludesCachedTokens ? inputTokens : inputTokens - cacheReadInputTokens - cacheWriteInputTokens, - ) + // AI SDK v6 normalized inputTokens to include cached tokens across all providers + // (including Anthropic/Bedrock which previously excluded them). Always subtract cache + // tokens to get the non-cached input count for separate cost calculation. + const adjustedInputTokens = safe(inputTokens - cacheReadInputTokens - cacheWriteInputTokens) - const total = iife(() => { - // Anthropic doesn't provide total_tokens, also ai sdk will vastly undercount if we - // don't compute from components - if ( - input.model.api.npm === "@ai-sdk/anthropic" || - input.model.api.npm === "@ai-sdk/amazon-bedrock" || - input.model.api.npm === "@ai-sdk/google-vertex/anthropic" - ) { - return adjustedInputTokens + outputTokens + cacheReadInputTokens + cacheWriteInputTokens - } - return input.usage.totalTokens - }) + const total = input.usage.totalTokens const tokens = { total, @@ -472,13 +450,10 @@ export namespace Session { const cfg = yield* config.get() if (!result.parentID && (Flag.KILO_AUTO_SHARE || cfg.share === "auto")) { - // kilocode_change - // kilocode_change yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope)) } if (!Flag.KILO_EXPERIMENTAL_WORKSPACES) { - // kilocode_change // This only exist for backwards compatibility. We should not be // manually publishing this event; it is a sync event now yield* bus.publish(Event.Updated, { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 6462c2c309..b55757c187 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -38,6 +38,13 @@ interface FetchDecompressionError extends Error { path: string } +/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */ +interface FetchDecompressionError extends Error { + code: "ZlibError" + errno: number + path: string +} + export namespace MessageV2 { export function isMedia(mime: string) { return mime.startsWith("image/") || mime === "application/pdf" @@ -958,7 +965,7 @@ export namespace MessageV2 { ) return rows.map( (row) => - // kilcode_change - apply stripping to parts fetched individually as well to cover all read paths + // kilocode_change - apply stripping to parts fetched individually as well to cover all read paths stripPartMetadata({ ...row.data, id: row.id, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ef6530327b..af9884bb4a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1441,10 +1441,10 @@ export namespace SessionPrompt { // Original logic when experimental plan mode is disabled if (!Flag.KILO_EXPERIMENTAL_PLAN_MODE) { - // kilocode_change - inject plan file path so agent writes to .kilo/plans/ + // kilocode_change start - inject plan file path so agent writes to .kilo/plans/ await KiloSessionPrompt.insertPlanReminders({ agent: input.agent, session: input.session, userMessage }) const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") - // kilocode_change start - renamed from "build" to "code" + // kilocode_change - renamed from "build" to "code" if (wasPlan && input.agent.name === "code") { // kilocode_change end userMessage.parts.push({ diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index a3b69817fc..431d0d1421 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -27,7 +27,7 @@ export namespace Skill { // kilocode_change end const EXTERNAL_DIRS = [".claude", ".agents"] const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" - const KILO_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" // kilocode_change + const KILO_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" const SKILL_PATTERN = "**/SKILL.md" export const Info = z.object({ diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 16423cd273..e0a550d4c4 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -132,13 +132,13 @@ export namespace Snapshot { const remove = (file: string) => fs.remove(file).pipe(Effect.catch(() => Effect.void)) const locked = <A, E, R>(fx: Effect.Effect<A, E, R>) => lock(state.gitdir).withPermits(1)(fx) - // kilocode_change start — ACP guard: disable snapshots for ACP clients const enabled = Effect.fnUntraced(function* () { if (state.vcs !== "git") return false + // kilocode_change start - ACP guard: disable snapshots for ACP clients if (KiloSnapshot.acpDisabled()) return false + // kilocode_change end return (yield* config.get()).snapshot !== false }) - // kilocode_change end const excludes = Effect.fnUntraced(function* () { const result = yield* git(["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], { @@ -311,98 +311,35 @@ export namespace Snapshot { ) }) - // kilocode_change start — batched revert: group up to 100 files per git checkout - const revertSingle = Effect.fnUntraced(function* (op: KiloSnapshot.RevertOp) { - log.info("reverting", { file: op.file, hash: op.hash }) - const result = yield* git([...core, ...args(["checkout", op.hash, "--", op.file])], { - cwd: state.worktree, - }) - if (result.code === 0) return - const tree = yield* git([...core, ...args(["ls-tree", op.hash, "--", op.rel])], { - cwd: state.worktree, - }) - if (tree.code === 0 && tree.text.trim()) { - log.info("file existed in snapshot but checkout failed, keeping", { file: op.file, hash: op.hash }) - return - } - log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash }) - yield* remove(op.file) - }) - - const revertBatch = Effect.fnUntraced(function* (batch: KiloSnapshot.RevertOp[]) { - const hash = batch[0]!.hash - - const tree = yield* git( - [...quote, ...args(["ls-tree", "--name-only", hash, "--", ...batch.map((op) => op.rel)])], - { cwd: state.worktree }, - ) - - if (tree.code !== 0) { - log.info("batched ls-tree failed, falling back to single-file revert", { hash, files: batch.length }) - for (const op of batch) yield* revertSingle(op) - return - } - - const existing = new Set( - tree.text - .trim() - .split("\n") - .map((l) => l.trim()) - .filter(Boolean), - ) - - const toCheckout = batch.filter((op) => existing.has(op.rel)) - if (toCheckout.length) { - log.info("reverting", { hash, files: toCheckout.length }) - const result = yield* git( - [...core, ...args(["checkout", hash, "--", ...toCheckout.map((op) => op.file)])], - { cwd: state.worktree }, - ) - if (result.code !== 0) { - log.info("batched checkout failed, falling back to single-file revert", { - hash, - files: toCheckout.length, - }) - for (const op of batch) yield* revertSingle(op) - return - } - } - - for (const op of batch) { - if (existing.has(op.rel)) continue - log.info("file did not exist in snapshot, deleting", { file: op.file, hash: op.hash }) - yield* remove(op.file) - } - }) - const revert = Effect.fnUntraced(function* (patches: Snapshot.Patch[]) { return yield* locked( Effect.gen(function* () { - const ops: KiloSnapshot.RevertOp[] = [] const seen = new Set<string>() for (const item of patches) { for (const file of item.files) { if (seen.has(file)) continue seen.add(file) - ops.push({ - hash: item.hash, - file, - rel: path.relative(state.worktree, file).replaceAll("\\", "/"), + log.info("reverting", { file, hash: item.hash }) + const result = yield* git([...core, ...args(["checkout", item.hash, "--", file])], { + cwd: state.worktree, }) - } - } - - for (const batch of KiloSnapshot.groupIntoBatches(ops)) { - if (batch.length === 1) { - yield* revertSingle(batch[0]!) - } else { - yield* revertBatch(batch) + if (result.code !== 0) { + const rel = path.relative(state.worktree, file) + const tree = yield* git([...core, ...args(["ls-tree", item.hash, "--", rel])], { + cwd: state.worktree, + }) + if (tree.code === 0 && tree.text.trim()) { + log.info("file existed in snapshot but checkout failed, keeping", { file }) + } else { + log.info("file did not exist in snapshot, deleting", { file }) + yield* remove(file) + } + } } } }), ) }) - // kilocode_change end const diff = Effect.fnUntraced(function* (hash: string) { return yield* locked( @@ -454,24 +391,7 @@ export namespace Snapshot { const [adds, dels, file] = line.split("\t") if (!file) continue const binary = adds === "-" && dels === "-" - // kilocode_change start — skip oversized files - let skip = binary - if (!binary) { - const [fromSize, toSize] = yield* Effect.all( - [ - git(["--git-dir", state.gitdir, "cat-file", "-s", `${from}:${file}`]).pipe( - Effect.map((r) => parseInt(r.text) || 0), - ), - git(["--git-dir", state.gitdir, "cat-file", "-s", `${to}:${file}`]).pipe( - Effect.map((r) => parseInt(r.text) || 0), - ), - ], - { concurrency: 2 }, - ) - skip = KiloSnapshot.oversized(fromSize) || KiloSnapshot.oversized(toSize) - } - // kilocode_change end - const [before, after] = skip + const [before, after] = binary ? ["", ""] : yield* Effect.all( [ diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 47e1f3326e..41aff208bd 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -115,10 +115,8 @@ export namespace ToolRegistry { const all = Effect.fn("ToolRegistry.all")(function* (custom: Tool.Info[]) { const cfg = yield* config.get() - // kilcoode_change start const question = ["app", "cli", "desktop", "vscode"].includes(Flag.KILO_CLIENT) || Flag.KILO_ENABLE_QUESTION_TOOL - // kilocode_change end return [ InvalidTool, diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index b27632eb8c..9ae382abe1 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -966,7 +966,7 @@ test("serializes config dependency installs across dirs", async () => { await gate } } - const mod = path.join(opts?.cwd ?? "", "node_modules", "@kilocode", "plugin") + const mod = path.join(opts?.cwd ?? "", "node_modules", "@kilocode", "plugin") // kilocode_change await fs.mkdir(mod, { recursive: true }) await Filesystem.write( path.join(mod, "package.json"), @@ -2031,6 +2031,7 @@ describe("KILO_DISABLE_PROJECT_CONFIG", () => { }) test("skips project .kilo/ directories when flag is set", async () => { + // kilocode_change const originalEnv = process.env["KILO_DISABLE_PROJECT_CONFIG"] process.env["KILO_DISABLE_PROJECT_CONFIG"] = "true" diff --git a/packages/opencode/test/control-plane/session-proxy-middleware.test.ts b/packages/opencode/test/control-plane/session-proxy-middleware.test.ts deleted file mode 100644 index 9cb4395cff..0000000000 --- a/packages/opencode/test/control-plane/session-proxy-middleware.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -// kilocode_change - retained; upstream removed workspace-server subsystem in #19316, routing refactor incomplete in v1.3.5 -import { afterEach, describe, expect, mock, test } from "bun:test" -import { WorkspaceID } from "../../src/control-plane/schema" -import { Hono } from "hono" -import { tmpdir } from "../fixture/fixture" -import { Project } from "../../src/project/project" -import { WorkspaceTable } from "../../src/control-plane/workspace.sql" -import { Instance } from "../../src/project/instance" -import { WorkspaceContext } from "../../src/control-plane/workspace-context" -import { Database } from "../../src/storage/db" -import { resetDatabase } from "../fixture/db" -import * as adaptors from "../../src/control-plane/adaptors" -import type { Adaptor } from "../../src/control-plane/types" -import { Flag } from "../../src/flag/flag" - -afterEach(async () => { - mock.restore() - await resetDatabase() -}) - -const original = Flag.KILO_EXPERIMENTAL_WORKSPACES -// @ts-expect-error don't do this normally, but it works -Flag.KILO_EXPERIMENTAL_WORKSPACES = true - -afterEach(() => { - // @ts-expect-error don't do this normally, but it works - Flag.KILO_EXPERIMENTAL_WORKSPACES = original -}) - -type State = { - workspace?: "first" | "second" - calls: Array<{ method: string; url: string; body?: string }> -} - -const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert - -async function setup(state: State) { - const TestAdaptor: Adaptor = { - configure(config) { - return config - }, - async create() { - throw new Error("not used") - }, - async remove() {}, - - async fetch(_config: unknown, input: RequestInfo | URL, init?: RequestInit) { - const url = - input instanceof Request || input instanceof URL - ? input.toString() - : new URL(input, "http://workspace.test").toString() - const request = new Request(url, init) - const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.text() - state.calls.push({ - method: request.method, - url: `${new URL(request.url).pathname}${new URL(request.url).search}`, - body, - }) - return new Response("proxied", { status: 202 }) - }, - } - - adaptors.installAdaptor("testing", TestAdaptor) - - await using tmp = await tmpdir({ git: true }) - const { project } = await Project.fromDirectory(tmp.path) - - const id1 = WorkspaceID.ascending() - const id2 = WorkspaceID.ascending() - - Database.use((db) => - db - .insert(WorkspaceTable) - .values([ - { - id: id1, - branch: "main", - project_id: project.id, - type: remote.type, - name: remote.name, - }, - { - id: id2, - branch: "main", - project_id: project.id, - type: "worktree", - directory: tmp.path, - name: "local", - }, - ]) - .run(), - ) - - const { WorkspaceRouterMiddleware } = await import("../../src/control-plane/workspace-router-middleware") - const app = new Hono().use(WorkspaceRouterMiddleware) - - return { - id1, - id2, - app, - async request(input: RequestInfo | URL, init?: RequestInit) { - return Instance.provide({ - directory: tmp.path, - fn: async () => - WorkspaceContext.provide({ - workspaceID: state.workspace === "first" ? id1 : id2, - fn: () => app.request(input, init), - }), - }) - }, - } -} - -describe("control-plane/session-proxy-middleware", () => { - test("forwards non-GET session requests for workspaces", async () => { - const state: State = { - workspace: "first", - calls: [], - } - - const ctx = await setup(state) - - ctx.app.post("/session/foo", (c) => c.text("local", 200)) - const response = await ctx.request("http://workspace.test/session/foo?x=1", { - method: "POST", - body: JSON.stringify({ hello: "world" }), - headers: { - "content-type": "application/json", - }, - }) - - expect(response.status).toBe(202) - expect(await response.text()).toBe("proxied") - expect(state.calls).toEqual([ - { - method: "POST", - url: "/session/foo?x=1", - body: '{"hello":"world"}', - }, - ]) - }) - - // It will behave this way when we have syncing - // - // test("does not forward GET requests", async () => { - // const state: State = { - // workspace: "first", - // calls: [], - // } - - // const ctx = await setup(state) - - // ctx.app.get("/session/foo", (c) => c.text("local", 200)) - // const response = await ctx.request("http://workspace.test/session/foo?x=1") - - // expect(response.status).toBe(200) - // expect(await response.text()).toBe("local") - // expect(state.calls).toEqual([]) - // }) -}) diff --git a/packages/opencode/test/control-plane/workspace-server-sse.test.ts b/packages/opencode/test/control-plane/workspace-server-sse.test.ts deleted file mode 100644 index acd840d1ee..0000000000 --- a/packages/opencode/test/control-plane/workspace-server-sse.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -// kilocode_change - retained; upstream removed workspace-server subsystem in #19316, routing refactor incomplete in v1.3.5 -import { afterEach, describe, expect, test } from "bun:test" -import { Log } from "../../src/util/log" -import { WorkspaceServer } from "../../src/control-plane/workspace-server/server" -import { parseSSE } from "../../src/control-plane/sse" -import { GlobalBus } from "../../src/bus/global" -import { resetDatabase } from "../fixture/db" -import { tmpdir } from "../fixture/fixture" - -afterEach(async () => { - await resetDatabase() -}) - -Log.init({ print: false }) - -describe("control-plane/workspace-server SSE", () => { - test("streams GlobalBus events and parseSSE reads them", async () => { - await using tmp = await tmpdir({ git: true }) - const app = WorkspaceServer.App() - const stop = new AbortController() - const seen: unknown[] = [] - try { - const response = await app.request("/event", { - signal: stop.signal, - headers: { - "x-kilo-workspace": "wrk_test_workspace", - "x-kilo-directory": tmp.path, - }, - }) - - expect(response.status).toBe(200) - expect(response.body).toBeDefined() - - const done = new Promise<void>((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error("timed out waiting for workspace.test event")) - }, 3000) - - void parseSSE(response.body!, stop.signal, (event) => { - seen.push(event) - const next = event as { type?: string } - if (next.type === "server.connected") { - GlobalBus.emit("event", { - payload: { - type: "workspace.test", - properties: { ok: true }, - }, - }) - return - } - if (next.type !== "workspace.test") return - clearTimeout(timeout) - resolve() - }).catch((error) => { - clearTimeout(timeout) - reject(error) - }) - }) - - await done - - expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true) - expect(seen).toContainEqual({ - type: "workspace.test", - properties: { ok: true }, - }) - } finally { - stop.abort() - } - }) -}) diff --git a/packages/opencode/test/plugin/trigger.test.ts b/packages/opencode/test/plugin/trigger.test.ts new file mode 100644 index 0000000000..7e52768529 --- /dev/null +++ b/packages/opencode/test/plugin/trigger.test.ts @@ -0,0 +1,111 @@ +import { afterAll, afterEach, describe, expect, test } from "bun:test" +import path from "path" +import { pathToFileURL } from "url" +import { tmpdir } from "../fixture/fixture" + +const disableDefault = process.env.KILO_DISABLE_DEFAULT_PLUGINS +process.env.KILO_DISABLE_DEFAULT_PLUGINS = "1" + +const { Plugin } = await import("../../src/plugin/index") +const { Instance } = await import("../../src/project/instance") + +afterEach(async () => { + await Instance.disposeAll() +}) + +afterAll(() => { + if (disableDefault === undefined) { + delete process.env.KILO_DISABLE_DEFAULT_PLUGINS + return + } + process.env.KILO_DISABLE_DEFAULT_PLUGINS = disableDefault +}) + +async function project(source: string) { + return tmpdir({ + init: async (dir) => { + const file = path.join(dir, "plugin.ts") + await Bun.write(file, source) + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify( + { + $schema: "https://opencode.ai/config.json", + plugin: [pathToFileURL(file).href], + }, + null, + 2, + ), + ) + }, + }) +} + +describe("plugin.trigger", () => { + test("runs synchronous hooks without crashing", async () => { + await using tmp = await project( + [ + "export default async () => ({", + ' "experimental.chat.system.transform": (_input, output) => {', + ' output.system.unshift("sync")', + " },", + "})", + "", + ].join("\n"), + ) + + const out = await Instance.provide({ + directory: tmp.path, + fn: async () => { + const out = { system: [] as string[] } + await Plugin.trigger( + "experimental.chat.system.transform", + { + model: { + providerID: "anthropic", + modelID: "claude-sonnet-4-6", + } as any, + }, + out, + ) + return out + }, + }) + + expect(out.system).toEqual(["sync"]) + }) + + test("awaits asynchronous hooks", async () => { + await using tmp = await project( + [ + "export default async () => ({", + ' "experimental.chat.system.transform": async (_input, output) => {', + " await Bun.sleep(1)", + ' output.system.unshift("async")', + " },", + "})", + "", + ].join("\n"), + ) + + const out = await Instance.provide({ + directory: tmp.path, + fn: async () => { + const out = { system: [] as string[] } + await Plugin.trigger( + "experimental.chat.system.transform", + { + model: { + providerID: "anthropic", + modelID: "claude-sonnet-4-6", + } as any, + }, + out, + ) + return out + }, + }) + + expect(out.system).toEqual(["async"]) + }) +}) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 7b4b71df25..51d228689a 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -964,8 +964,9 @@ describe("session.getUsage", () => { expect(result.tokens.cache.write).toBe(300) }) - test("does not subtract cached tokens for anthropic provider", () => { + test("subtracts cached tokens for anthropic provider", () => { const model = createModel({ context: 100_000, output: 32_000 }) + // AI SDK v6 normalizes inputTokens to include cached tokens for all providers const result = Session.getUsage({ model, usage: { @@ -979,7 +980,7 @@ describe("session.getUsage", () => { }, }) - expect(result.tokens.input).toBe(1000) + expect(result.tokens.input).toBe(800) expect(result.tokens.cache.read).toBe(200) }) @@ -1267,11 +1268,10 @@ describe("session.getUsage", () => { "computes total from components for %s models", (npm) => { const model = createModel({ context: 100_000, output: 32_000, npm }) + // AI SDK v6: inputTokens includes cached tokens for all providers const usage = { inputTokens: 1000, outputTokens: 500, - // These providers typically report total as input + output only, - // excluding cache read/write. totalTokens: 1500, cachedInputTokens: 200, } @@ -1288,10 +1288,12 @@ describe("session.getUsage", () => { }, }) - expect(result.tokens.input).toBe(1000) + // inputTokens (1000) includes cache, so adjusted = 1000 - 200 - 300 = 500 + expect(result.tokens.input).toBe(500) expect(result.tokens.cache.read).toBe(200) expect(result.tokens.cache.write).toBe(300) - expect(result.tokens.total).toBe(2000) + // total = adjusted (500) + output (500) + cacheRead (200) + cacheWrite (300) + expect(result.tokens.total).toBe(1500) return } @@ -1305,10 +1307,12 @@ describe("session.getUsage", () => { }, }) - expect(result.tokens.input).toBe(1000) + // inputTokens (1000) includes cache, so adjusted = 1000 - 200 - 300 = 500 + expect(result.tokens.input).toBe(500) expect(result.tokens.cache.read).toBe(200) expect(result.tokens.cache.write).toBe(300) - expect(result.tokens.total).toBe(2000) + // total = adjusted (500) + output (500) + cacheRead (200) + cacheWrite (300) + expect(result.tokens.total).toBe(1500) }, ) }) diff --git a/packages/script/src/index.ts b/packages/script/src/index.ts index ff7456f8cd..24f0d4875e 100644 --- a/packages/script/src/index.ts +++ b/packages/script/src/index.ts @@ -158,7 +158,7 @@ export const Script = { return IS_PREVIEW }, get release(): boolean { - return !!env.KILO_RELEASE // kilocode_change + return !!env.KILO_RELEASE }, get team() { return team diff --git a/packages/sdk/js/src/client.ts b/packages/sdk/js/src/client.ts index 7a233f431a..6d41dad433 100644 --- a/packages/sdk/js/src/client.ts +++ b/packages/sdk/js/src/client.ts @@ -24,7 +24,7 @@ function rewrite(request: Request, directory?: string) { url.searchParams.set("directory", value) } - const next = new Request(url.href, request) // kilocode_change - Bun Request() only accepts string | Request + const next = new Request(url, request) next.headers.delete("x-kilo-directory") return next } diff --git a/packages/sdk/js/src/v2/client.ts b/packages/sdk/js/src/v2/client.ts index 0b5d64c0ec..61bb1f19f8 100644 --- a/packages/sdk/js/src/v2/client.ts +++ b/packages/sdk/js/src/v2/client.ts @@ -37,7 +37,7 @@ function rewrite(request: Request, values: { directory?: string; workspace?: str if (!changed) return request - const next = new Request(url.href, request) // kilocode_change - Bun Request() only accepts string | Request + const next = new Request(url, request) next.headers.delete("x-kilo-directory") next.headers.delete("x-kilo-workspace") return next