mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
resolve merge conflicts
This commit is contained in:
@@ -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"]
|
||||
|
||||
+19
-20
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,8 @@ import {
|
||||
import { Popover } from "@kilocode/kilo-ui/popover"
|
||||
import type { PopoverProps } from "@kilocode/kilo-ui/popover"
|
||||
|
||||
export interface PopupSelectorProps<T extends ValidComponent = ValidComponent> extends Omit<
|
||||
PopoverProps<T>,
|
||||
"style" | "children"
|
||||
> {
|
||||
export interface PopupSelectorProps<T extends ValidComponent = ValidComponent>
|
||||
extends Omit<PopoverProps<T>, "style" | "children"> {
|
||||
/** Whether the selector is in expanded mode (wider + taller). */
|
||||
expanded: boolean
|
||||
/** Preferred width when collapsed. Default: 250 */
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -24,7 +24,6 @@ export function DialogVariant() {
|
||||
title={"Select variant"}
|
||||
current={local.model.variant.current()}
|
||||
flat={true}
|
||||
skipFilter={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<Session> }) => {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bold: true }}>#</span> <span style={{ bold: true }}>{props.session().title}</span>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
|
||||
const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Accessor<string> }) => {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<Show when={props.context()}>
|
||||
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
|
||||
{props.context()} ({props.cost()})
|
||||
</text>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkspaceInfo = (props: { workspace: Accessor<string | undefined> }) => {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<Show when={props.workspace()}>
|
||||
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
|
||||
{props.workspace()}
|
||||
</text>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<box flexShrink={0}>
|
||||
<box
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={1}
|
||||
{...SplitBorder}
|
||||
border={["left"]}
|
||||
borderColor={theme.border}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={session()?.parentID}>
|
||||
<box flexDirection="column" gap={1}>
|
||||
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={narrow() ? 1 : 0}>
|
||||
{Flag.KILO_EXPERIMENTAL_WORKSPACES ? (
|
||||
<box flexDirection="column">
|
||||
<text fg={theme.text}>
|
||||
<b>Subagent session</b>
|
||||
</text>
|
||||
<WorkspaceInfo workspace={workspace} />
|
||||
</box>
|
||||
) : (
|
||||
<text fg={theme.text}>
|
||||
<b>Subagent session</b>
|
||||
</text>
|
||||
)}
|
||||
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
</box>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<box
|
||||
onMouseOver={() => setHover("parent")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => command.trigger("session.parent")}
|
||||
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
onMouseOver={() => setHover("prev")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => command.trigger("session.child.previous")}
|
||||
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
|
||||
</text>
|
||||
</box>
|
||||
<box
|
||||
onMouseOver={() => setHover("next")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => command.trigger("session.child.next")}
|
||||
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<box flexDirection={narrow() ? "column" : "row"} justifyContent="space-between" gap={1}>
|
||||
{Flag.KILO_EXPERIMENTAL_WORKSPACES ? (
|
||||
<box flexDirection="column">
|
||||
<Title session={session} />
|
||||
<WorkspaceInfo workspace={workspace} />
|
||||
</box>
|
||||
) : (
|
||||
<Title session={session} />
|
||||
)}
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -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>>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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([])
|
||||
// })
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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"])
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user