fix(opencode): complete v1.17.4 compatibility

This commit is contained in:
Johnny Amancio
2026-07-14 14:51:11 +02:00
parent 68b59571a0
commit 4e2d04a17e
100 changed files with 11689 additions and 4011 deletions
+57
View File
@@ -12,6 +12,7 @@ import { FSUtil } from "./fs-util"
import { Global } from "./global"
import { DataMigrationTable } from "./data-migration.sql"
import path from "path"
import { parse as parseKiloAccounts } from "./kilocode/credential-migration" // kilocode_change
export const ID = Schema.String.pipe(
Schema.brand("Credential.ID"),
@@ -106,6 +107,62 @@ export const legacyImportLayer = Layer.effectDiscard(
const { db } = yield* Database.Service
const fs = yield* FSUtil.Service
const global = yield* Global.Service
// kilocode_change start - preserve Kilo's multi-account JSON stores before the upstream auth.json fallback
const kiloName = "credential.kilo-account-json"
if (!(yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, kiloName)).get())) {
const current = yield* fs.readJson(path.join(global.data, "account.json")).pipe(Effect.option)
const prior = yield* fs.readJson(path.join(global.data, "auth-v2.json")).pipe(Effect.option)
const raw = Option.isSome(current) ? current.value : Option.getOrUndefined(prior)
const values = parseKiloAccounts(raw)
if (values.length > 0) {
yield* db.transaction((tx) =>
Effect.gen(function* () {
const existing = new Set(
(yield* tx.select({ connectorID: CredentialTable.connector_id }).from(CredentialTable).all()).map(
(item) => item.connectorID,
),
)
for (const item of values) {
const connector = ConnectorSchema.ID.make(item.connectorID.replace(/\/+$/, ""))
if (existing.has(connector)) continue
const value: Value =
item.credential.type === "api"
? new Key({
type: "key",
key: item.credential.key,
metadata: item.credential.metadata,
})
: new OAuth({
type: "oauth",
refresh: item.credential.refresh,
access: item.credential.access,
expires: item.credential.expires,
metadata: {
...(item.credential.accountId ? { accountID: item.credential.accountId } : {}),
...(item.credential.enterpriseUrl ? { enterpriseURL: item.credential.enterpriseUrl } : {}),
},
})
yield* tx.insert(CredentialTable).values({
id: ID.create(),
connector_id: connector,
method_id: ConnectorSchema.MethodID.make(
item.credential.type === "api"
? "api-key"
: connector === ConnectorSchema.ID.make("openai")
? "chatgpt-browser"
: "oauth",
),
label: item.label,
value,
active: item.active,
})
}
yield* tx.insert(DataMigrationTable).values({ name: kiloName, time_completed: Date.now() }).run()
}),
)
}
}
// kilocode_change end
const name = "credential.auth-json"
if (yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, name)).get()) return
const raw = yield* fs.readJson(path.join(global.data, "auth.json")).pipe(Effect.option)
@@ -0,0 +1,48 @@
import { Option, Schema } from "effect"
import { NonNegativeInt } from "../schema"
const OAuth = Schema.Struct({
type: Schema.Literal("oauth"),
refresh: Schema.String,
access: Schema.String,
expires: NonNegativeInt,
accountId: Schema.optional(Schema.String),
enterpriseUrl: Schema.optional(Schema.String),
})
const Key = Schema.Struct({
type: Schema.Literal("api"),
key: Schema.String,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
})
const Account = Schema.Struct({
id: Schema.String,
serviceID: Schema.String,
description: Schema.String,
credential: Schema.Union([OAuth, Key]),
})
const Store = Schema.Struct({
version: Schema.Literal(2),
accounts: Schema.Record(Schema.String, Account),
active: Schema.Record(Schema.String, Schema.String),
})
export function parse(input: unknown) {
const decoded = Schema.decodeUnknownOption(Store)(input)
if (Option.isNone(decoded)) return []
const first = new Set<string>()
return Object.values(decoded.value.accounts).map((account) => {
const fallback = !first.has(account.serviceID)
first.add(account.serviceID)
return {
connectorID: account.serviceID,
label: account.description,
credential: account.credential,
active: decoded.value.active[account.serviceID]
? decoded.value.active[account.serviceID] === account.id
: fallback,
}
})
}
+232
View File
@@ -0,0 +1,232 @@
export * as Log from "./log"
import path from "path"
import { existsSync, writeFileSync } from "fs" // kilocode_change
import fs from "fs/promises"
import * as Global from "../global"
import { Schema } from "effect"
import { Glob } from "./glob"
import { createStream } from "rotating-file-stream" // kilocode_change
import { KILO_RUN_ID } from "./opencode-process" // kilocode_change
export const Level = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
identifier: "LogLevel",
description: "Log level",
})
export type Level = Schema.Schema.Type<typeof Level>
const levelPriority: Record<Level, number> = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
}
const keep = 10
const initializedRunID = "KILO_LOG_INITIALIZED_RUN_ID"
let level: Level = "INFO"
function shouldLog(input: Level): boolean {
return levelPriority[input] >= levelPriority[level]
}
export type Logger = {
debug(message?: any, extra?: Record<string, any>): void
info(message?: any, extra?: Record<string, any>): void
error(message?: any, extra?: Record<string, any>): void
warn(message?: any, extra?: Record<string, any>): void
tag(key: string, value: string): Logger
clone(): Logger
time(
message: string,
extra?: Record<string, any>,
): {
stop(): void
[Symbol.dispose](): void
}
}
const loggers = new Map<string, Logger>()
export const Default = create({ service: "default" })
export interface Options {
print: boolean
dev?: boolean
level?: Level
}
let logpath = ""
export function file() {
return logpath
}
const stderr = (msg: any) => {
process.stderr.write(msg)
return msg.length
}
let write = stderr
let stream: ReturnType<typeof createStream> | undefined // kilocode_change
export async function init(options: Options) {
if (options.level) level = options.level
void cleanup(Global.Path.log)
// kilocode_change start - initialize one rotating stream and truncate dev.log once per Kilo run
if (stream) {
const active = stream
stream = undefined
await new Promise<void>((resolve) => active.end(resolve))
}
if (options.print) {
write = stderr
return
}
logpath = path.join(
Global.Path.log,
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
const run = process.env[KILO_RUN_ID]
if (!options.dev || !run || process.env[initializedRunID] !== run) {
await fs.truncate(logpath).catch(() => {})
if (options.dev && run) process.env[initializedRunID] = run
}
const dir = path.dirname(logpath)
const active = createStream(path.basename(logpath), {
size: "50M",
maxFiles: 10,
history: ".log-history",
path: dir,
})
stream = active
active.on("rotation", () => {
if (!existsSync(dir)) return
try {
// RATIONALE: If current log path was deleted while stream still holds the fd,
// rotating-file-stream will try to rename a missing path and emit ENOENT.
writeFileSync(logpath, "", { flag: "wx" })
} catch (err) {
if (typeof err === "object" && err && "code" in err && err.code === "EEXIST") return
const msg = err instanceof Error ? err.message : String(err)
process.stderr.write("log stream warning: " + msg + "\n")
}
})
active.on("error", (err: Error) => {
process.stderr.write("log stream error: " + err.message + "\n")
})
active.on("warning", (err: Error) => {
process.stderr.write("log stream warning: " + err.message + "\n")
})
write = (msg: any) => {
active.write(msg)
return msg.length
}
// kilocode_change end
}
async function cleanup(dir: string) {
const files = (
await Glob.scan("????-??-??T??????.log", {
cwd: dir,
absolute: false,
include: "file",
}).catch(() => [])
)
.filter((file) => path.basename(file) === file)
.sort()
if (files.length <= keep) return
const doomed = files.slice(0, -keep)
await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {})))
}
function formatError(error: Error, depth = 0): string {
const result = error.message
return error.cause instanceof Error && depth < 10
? result + " Caused by: " + formatError(error.cause, depth + 1)
: result
}
let last = Date.now()
export function create(tags?: Record<string, any>) {
tags = tags || {}
const service = tags["service"]
if (service && typeof service === "string") {
const cached = loggers.get(service)
if (cached) {
return cached
}
}
function build(message: any, extra?: Record<string, any>) {
const prefix = Object.entries({
...tags,
...extra,
})
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
const prefix = `${key}=`
if (value instanceof Error) return prefix + formatError(value)
if (typeof value === "object") return prefix + JSON.stringify(value)
return prefix + value
})
.join(" ")
const next = new Date()
const diff = next.getTime() - last
last = next.getTime()
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
}
const result: Logger = {
debug(message?: any, extra?: Record<string, any>) {
if (shouldLog("DEBUG")) {
write("DEBUG " + build(message, extra))
}
},
info(message?: any, extra?: Record<string, any>) {
if (shouldLog("INFO")) {
write("INFO " + build(message, extra))
}
},
error(message?: any, extra?: Record<string, any>) {
if (shouldLog("ERROR")) {
write("ERROR " + build(message, extra))
}
},
warn(message?: any, extra?: Record<string, any>) {
if (shouldLog("WARN")) {
write("WARN " + build(message, extra))
}
},
tag(key: string, value: string) {
if (tags) tags[key] = value
return result
},
clone() {
return create({ ...tags })
},
time(message: string, extra?: Record<string, any>) {
const now = Date.now()
result.info(message, { status: "started", ...extra })
function stop() {
result.info(message, {
status: "completed",
duration: Date.now() - now,
...extra,
})
}
return {
stop,
[Symbol.dispose]() {
stop()
},
}
},
}
if (service && typeof service === "string") {
loggers.set(service, result)
}
return result
}
@@ -0,0 +1,24 @@
export const KILO_RUN_ID = "KILO_RUN_ID"
export const KILO_PROCESS_ROLE = "KILO_PROCESS_ROLE"
export function ensureRunID() {
return (process.env[KILO_RUN_ID] ??= crypto.randomUUID())
}
export function ensureProcessRole(fallback: "main" | "worker") {
return (process.env[KILO_PROCESS_ROLE] ??= fallback)
}
export function ensureProcessMetadata(fallback: "main" | "worker") {
return {
runID: ensureRunID(),
processRole: ensureProcessRole(fallback),
}
}
export function sanitizedProcessEnv(overrides?: Record<string, string>) {
const env = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
)
return overrides ? Object.assign(env, overrides) : env
}
@@ -1,7 +1,9 @@
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Auth } from "@opencode-ai/core/auth"
import { Connector } from "@opencode-ai/core/connector"
import { Credential } from "@opencode-ai/core/credential"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
@@ -9,11 +11,17 @@ import { tmpdir } from "../fixture/tmpdir"
import { it } from "../lib/effect"
function layer(dir: string) {
return Auth.layer.pipe(
const database = Database.layerFromPath(path.join(dir, "credential.db")).pipe(Layer.fresh)
const importer = Credential.legacyImportLayer.pipe(
Layer.provide(database),
Layer.provide(FSUtil.defaultLayer),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provide(Global.layerWith({ data: dir })),
)
return Credential.layer.pipe(
Layer.provide(database),
Layer.provide(EventV2.defaultLayer),
Layer.provideMerge(importer),
)
}
const auth = Effect.acquireRelease(
@@ -29,7 +37,7 @@ const auth = Effect.acquireRelease(
}),
)
describe("Auth auth-v2 migration", () => {
describe("Credential auth-v2 migration", () => {
it.live("preserves multiple accounts, active selection, and Kilo organization", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
@@ -72,22 +80,20 @@ describe("Auth auth-v2 migration", () => {
yield* Effect.promise(() => Bun.write(path.join(tmp.path, "auth-v2.json"), JSON.stringify(store)))
const result = yield* Effect.gen(function* () {
const accounts = yield* Auth.Service
const credentials = yield* Credential.Service
return {
all: yield* accounts.all(),
active: yield* accounts.active(Auth.ServiceID.make("kilo")),
all: yield* credentials.all(),
active: yield* credentials.active(Connector.ID.make("kilo")),
}
}).pipe(Effect.provide(layer(tmp.path)))
expect(result.all.map((item) => String(item.id))).toEqual(["acc_first", "acc_second"])
expect(String(result.active?.id)).toBe("acc_second")
expect(result.active?.credential.type).toBe("oauth")
if (result.active?.credential.type === "oauth") {
expect(result.active.credential.access).toBe("access-second")
expect(result.active.credential.accountId).toBe("org-second")
expect(result.all.map((item) => item.label)).toEqual(["first", "second"])
expect(result.active?.label).toBe("second")
expect(result.active?.value.type).toBe("oauth")
if (result.active?.value.type === "oauth") {
expect(result.active.value.access).toBe("access-second")
expect(result.active.value.metadata?.accountID).toBe("org-second")
}
const saved = yield* Effect.promise(() => Bun.file(path.join(tmp.path, "account.json")).json())
expect(saved).toEqual(store)
}),
),
),
@@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo"
@@ -150,7 +151,7 @@ describe("KiloPlugin", () => {
const transform = yield* catalog.transform()
yield* transform((catalog) => {
const item = provider("kilo", {
enabled: { via: "account", service: "kilo" },
enabled: { via: "credential", credentialID: Credential.ID.make("cred_kilo") },
request: {
headers: {},
body: { apiKey: "authenticated-token", kilocodeOrganizationId: "authenticated-org" },
@@ -163,7 +164,7 @@ describe("KiloPlugin", () => {
})
const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo"))
expect(result.enabled).toEqual({ via: "account", service: "kilo" })
expect(result.enabled).toEqual({ via: "credential", credentialID: Credential.ID.make("cred_kilo") })
expect(result.request.body.kilocodeToken).toBe("authenticated-token")
expect(result.request.body.kilocodeOrganizationId).toBe("environment-org")
}),
@@ -29,7 +29,7 @@ Options:
manage MCP (Model Context Protocol) servers
Commands:
kilo mcp add add an MCP server
kilo mcp add [name] add an MCP server
kilo mcp list list MCP servers and their status [aliases: ls]
kilo mcp auth [name] authenticate with an OAuth-enabled MCP server
kilo mcp logout [name] remove OAuth credentials for an MCP server
@@ -45,9 +45,15 @@ Options:
```
add an MCP server
Positionals:
name name of the MCP server [string]
Options:
--help Show help [boolean]
--version Show version number [boolean]
--url URL for a remote MCP server [string]
--env environment variable for a local MCP server (KEY=VALUE) [array]
--header HTTP header for a remote MCP server (KEY=VALUE) [array]
```
### kilo mcp list
@@ -289,7 +295,6 @@ Options:
ripgrep debugging utilities
Commands:
kilo debug rg tree show file tree using ripgrep
kilo debug rg files list files using ripgrep
kilo debug rg search <pattern> search file contents using ripgrep
@@ -298,17 +303,6 @@ Options:
--version Show version number [boolean]
```
### kilo debug rg tree
```
show file tree using ripgrep
Options:
--help Show help [boolean]
--version Show version number [boolean]
--limit [number]
```
### kilo debug rg files
```
@@ -346,7 +340,6 @@ Commands:
kilo debug file read <path> read file contents as JSON
kilo debug file list <path> list files in a directory
kilo debug file search <query> search files by query
kilo debug file tree [dir] show directory tree
Options:
--help Show help [boolean]
@@ -392,19 +385,6 @@ Options:
--version Show version number [boolean]
```
### kilo debug file tree
```
show directory tree
Positionals:
dir Directory to tree [string] [default: "."]
Options:
--help Show help [boolean]
--version Show version number [boolean]
```
### kilo debug scrap
```
@@ -547,9 +527,9 @@ Options:
manage AI providers and credentials
Commands:
kilo auth list list providers and credentials [aliases: ls]
kilo auth login [url] log in to a provider
kilo auth logout log out from a configured provider
kilo auth list list providers and credentials [aliases: ls]
kilo auth login [url] log in to a provider
kilo auth logout [provider] log out from a configured provider
Options:
--help Show help [boolean]
@@ -586,6 +566,9 @@ Options:
```
log out from a configured provider
Positionals:
provider provider id or name to log out from [string]
Options:
--help Show help [boolean]
--version Show version number [boolean]
+363 -364
View File
@@ -29,9 +29,8 @@ import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
import * as KiloAgent from "@/kilocode/agent" // kilocode_change
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Reference as ReferenceV1 } from "@/reference/reference" // kilocode_change
import { ConfigReference } from "@/config/reference" // kilocode_change
import * as AgentRequirements from "@/kilocode/agent-requirements" // kilocode_change
import * as KiloReference from "@/kilocode/reference" // kilocode_change
import { MCP } from "@/mcp" // kilocode_change
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -114,398 +113,398 @@ export const layer = Layer.effect(
const locations = yield* LocationServiceMap
const state = yield* InstanceState.make<State>(
Effect.fn("Agent.state")(function* (ctx) {
const cfg = yield* config.get()
const skillDirs = yield* skill.dirs()
// kilocode_change start - include global config dirs so agents can read them without prompting
const referenceDirs = yield* Effect.gen(function* () {
Effect.fn("Agent.state")(function* (ctx) {
const cfg = yield* config.get()
const skillDirs = yield* skill.dirs()
// kilocode_change start - include global config dirs so agents can read them without prompting
const referenceDirs = yield* Effect.gen(function* () {
yield* (yield* PluginBoot.Service).wait()
return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
const whitelistedDirs = [
Truncate.GLOB,
path.join(Global.Path.tmp, "*"),
...skillDirs.map((dir) => path.join(dir, "*")),
path.join(Global.Path.config, "*"),
Truncate.GLOB,
path.join(Global.Path.tmp, "*"),
...skillDirs.map((dir) => path.join(dir, "*")),
path.join(Global.Path.config, "*"),
...KilocodePaths.globalDirs().map((dir) => path.join(dir, "*")),
...referenceDirs.map((dir) => path.join(dir, "*")),
...referenceDirs.map((dir) => path.join(dir, "*")),
]
// kilocode_change end
const readonlyExternalDirectory = {
"*": "ask",
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
} satisfies Record<string, "allow" | "ask" | "deny">
const baseDefaults = Permission.fromConfig({
// kilocode_change
"*": "allow",
doom_loop: "ask",
external_directory: {
// kilocode_change end
const readonlyExternalDirectory = {
"*": "ask",
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
},
suggest: "deny", // kilocode_change
question: "deny",
interactive_terminal: "deny", // kilocode_change - human-driven tools are primary-agent only
plan_enter: "deny",
plan_exit: "deny",
repo_clone: "deny", // kilocode_change
repo_overview: "deny", // kilocode_change
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: {
} satisfies Record<string, "allow" | "ask" | "deny">
const baseDefaults = Permission.fromConfig({
// kilocode_change
"*": "allow",
"*.env": "ask",
"*.env.*": "ask",
"*.env.example": "allow",
},
})
doom_loop: "ask",
external_directory: {
"*": "ask",
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
},
suggest: "deny", // kilocode_change
question: "deny",
interactive_terminal: "deny", // kilocode_change - human-driven tools are primary-agent only
plan_enter: "deny",
plan_exit: "deny",
repo_clone: "deny", // kilocode_change
repo_overview: "deny", // kilocode_change
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: {
"*": "allow",
"*.env": "ask",
"*.env.*": "ask",
"*.env.example": "allow",
},
})
// kilocode_change start - patch defaults with bash allowlist and recall permission
const kilo = KiloAgent.prepare(cfg)
const defaults = Permission.merge(baseDefaults, kilo.defaultsPatch)
// kilocode_change end
const user = Permission.fromConfig(cfg.permission ?? {})
const agents: Record<string, Info> = {
build: {
name: "build",
description: "The default agent. Executes tools based on configured permissions.",
options: {},
permission: Permission.merge(
defaults,
Permission.fromConfig({
question: "allow",
interactive_terminal: "allow", // kilocode_change
suggest: "allow", // kilocode_change
plan_enter: "allow",
}),
user,
),
mode: "primary",
native: true,
},
plan: {
name: "plan",
description: "Plan mode. Disallows all edit tools.",
options: {},
permission: Permission.merge(
defaults,
Permission.fromConfig({
question: "allow",
plan_exit: "allow",
task: {
general: "deny",
},
external_directory: {
[path.join(Global.Path.data, "plans", "*")]: "allow",
},
edit: {
"*": "deny",
[path.join(".opencode", "plans", "*.md")]: "allow",
[path.relative(ctx.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
},
}),
user,
),
mode: "primary",
native: true,
},
general: {
name: "general",
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
permission: Permission.merge(
defaults,
Permission.fromConfig({
todowrite: "deny",
}),
user,
),
options: {},
mode: "subagent",
native: true,
},
explore: {
name: "explore",
permission: Permission.merge(
defaults,
Permission.fromConfig({
"*": "deny",
grep: "allow",
glob: "allow",
list: "allow",
bash: "allow",
webfetch: "allow",
websearch: "allow",
read: "allow",
external_directory: readonlyExternalDirectory,
}),
user,
),
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
prompt: PROMPT_EXPLORE,
options: {},
mode: "subagent",
native: true,
},
// kilocode_change start - retain Kilo's opt-in repository research agent
...(flags.experimentalScout
? {
scout: {
name: "scout",
permission: Permission.merge(
defaults,
Permission.fromConfig({
"*": "deny",
grep: "allow",
glob: "allow",
webfetch: "allow",
websearch: "allow",
read: "allow",
repo_clone: "allow",
repo_overview: "allow",
external_directory: {
...readonlyExternalDirectory,
[path.join(Global.Path.repos, "*")]: "allow",
},
}),
user,
),
description: `Docs and dependency-source specialist. Use this when you need to inspect external documentation, clone dependency repositories into the managed cache, and research library implementation details without modifying the user's workspace.`,
prompt: PROMPT_SCOUT,
options: {},
mode: "subagent" as const,
native: true,
},
}
: {}),
// kilocode_change start - patch defaults with bash allowlist and recall permission
const kilo = KiloAgent.prepare(cfg)
const defaults = Permission.merge(baseDefaults, kilo.defaultsPatch)
// kilocode_change end
compaction: {
name: "compaction",
mode: "primary",
native: true,
hidden: true,
prompt: PROMPT_COMPACTION,
permission: Permission.merge(
defaults,
user,
Permission.fromConfig({
"*": "deny",
}),
),
options: {},
},
title: {
name: "title",
mode: "primary",
options: {},
native: true,
hidden: true,
temperature: 0.5,
permission: Permission.merge(
defaults,
user,
Permission.fromConfig({
"*": "deny",
}),
),
prompt: PROMPT_TITLE,
},
summary: {
name: "summary",
mode: "primary",
options: {},
native: true,
hidden: true,
permission: Permission.merge(
defaults,
user,
Permission.fromConfig({
"*": "deny",
}),
),
prompt: PROMPT_SUMMARY,
},
}
// kilocode_change start - rename build→code, add debug/orchestrator/ask, patch plan/explore
KiloAgent.patchAgents(agents, defaults, user, cfg, kilo, ctx.worktree, whitelistedDirs)
const user = Permission.fromConfig(cfg.permission ?? {})
const agentConfigs = KiloAgent.preprocessConfig(cfg.agent ?? {})
for (const [key, value] of Object.entries(agentConfigs)) {
// kilocode_change end
if (value.disable) {
delete agents[key]
continue
}
let item = agents[key]
if (!item)
item = agents[key] = {
name: key,
mode: "all",
permission: Permission.merge(defaults, user),
const agents: Record<string, Info> = {
build: {
name: "build",
description: "The default agent. Executes tools based on configured permissions.",
options: {},
native: false,
}
if (value.model) item.model = Provider.parseModel(value.model)
item.variant = value.variant ?? item.variant
item.prompt = value.prompt ?? item.prompt
item.description = value.description ?? item.description
item.temperature = value.temperature ?? item.temperature
item.topP = value.top_p ?? item.topP
item.mode = value.mode ?? item.mode
item.color = value.color ?? item.color
item.hidden = value.hidden ?? item.hidden
item.name = value.name ?? item.name
item.steps = value.steps ?? item.steps
// kilocode_change start - carry metadata as typed fields, never as provider options
item.displayName = value.displayName ?? item.displayName
item.source = value.source ?? item.source
item.requirements = value.requirements ?? item.requirements
// kilocode_change end
item.options = mergeDeep(item.options, value.options ?? {})
item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
KiloAgent.processConfigItem(item) // kilocode_change - populate displayName from options
}
permission: Permission.merge(
defaults,
Permission.fromConfig({
question: "allow",
interactive_terminal: "allow", // kilocode_change
suggest: "allow", // kilocode_change
plan_enter: "allow",
}),
user,
),
mode: "primary",
native: true,
},
plan: {
name: "plan",
description: "Plan mode. Disallows all edit tools.",
options: {},
permission: Permission.merge(
defaults,
Permission.fromConfig({
question: "allow",
plan_exit: "allow",
task: {
general: "deny",
},
external_directory: {
[path.join(Global.Path.data, "plans", "*")]: "allow",
},
edit: {
"*": "deny",
[path.join(".opencode", "plans", "*.md")]: "allow",
[path.relative(ctx.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
},
}),
user,
),
mode: "primary",
native: true,
},
general: {
name: "general",
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
permission: Permission.merge(
defaults,
Permission.fromConfig({
todowrite: "deny",
}),
user,
),
options: {},
mode: "subagent",
native: true,
},
explore: {
name: "explore",
permission: Permission.merge(
defaults,
Permission.fromConfig({
"*": "deny",
grep: "allow",
glob: "allow",
list: "allow",
bash: "allow",
webfetch: "allow",
websearch: "allow",
read: "allow",
external_directory: readonlyExternalDirectory,
}),
user,
),
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
prompt: PROMPT_EXPLORE,
options: {},
mode: "subagent",
native: true,
},
// kilocode_change start - retain Kilo's opt-in repository research agent
...(flags.experimentalScout
? {
scout: {
name: "scout",
permission: Permission.merge(
defaults,
Permission.fromConfig({
"*": "deny",
grep: "allow",
glob: "allow",
webfetch: "allow",
websearch: "allow",
read: "allow",
repo_clone: "allow",
repo_overview: "allow",
external_directory: {
...readonlyExternalDirectory,
[path.join(Global.Path.repos, "*")]: "allow",
},
}),
user,
),
description: `Docs and dependency-source specialist. Use this when you need to inspect external documentation, clone dependency repositories into the managed cache, and research library implementation details without modifying the user's workspace.`,
prompt: PROMPT_SCOUT,
options: {},
mode: "subagent" as const,
native: true,
},
}
: {}),
// kilocode_change end
compaction: {
name: "compaction",
mode: "primary",
native: true,
hidden: true,
prompt: PROMPT_COMPACTION,
permission: Permission.merge(
defaults,
user,
Permission.fromConfig({
"*": "deny",
}),
),
options: {},
},
title: {
name: "title",
mode: "primary",
options: {},
native: true,
hidden: true,
temperature: 0.5,
permission: Permission.merge(
defaults,
user,
Permission.fromConfig({
"*": "deny",
}),
),
prompt: PROMPT_TITLE,
},
summary: {
name: "summary",
mode: "primary",
options: {},
native: true,
hidden: true,
permission: Permission.merge(
defaults,
user,
Permission.fromConfig({
"*": "deny",
}),
),
prompt: PROMPT_SUMMARY,
},
}
// kilocode_change start - rename build→code, add debug/orchestrator/ask, patch plan/explore
KiloAgent.patchAgents(agents, defaults, user, cfg, kilo, ctx.worktree, whitelistedDirs)
const agentConfigs = KiloAgent.preprocessConfig(cfg.agent ?? {})
for (const [key, value] of Object.entries(agentConfigs)) {
// kilocode_change end
if (value.disable) {
delete agents[key]
continue
}
let item = agents[key]
if (!item)
item = agents[key] = {
name: key,
mode: "all",
permission: Permission.merge(defaults, user),
options: {},
native: false,
}
if (value.model) item.model = Provider.parseModel(value.model)
item.variant = value.variant ?? item.variant
item.prompt = value.prompt ?? item.prompt
item.description = value.description ?? item.description
item.temperature = value.temperature ?? item.temperature
item.topP = value.top_p ?? item.topP
item.mode = value.mode ?? item.mode
item.color = value.color ?? item.color
item.hidden = value.hidden ?? item.hidden
item.name = value.name ?? item.name
item.steps = value.steps ?? item.steps
// kilocode_change start - carry metadata as typed fields, never as provider options
item.displayName = value.displayName ?? item.displayName
item.source = value.source ?? item.source
item.requirements = value.requirements ?? item.requirements
// kilocode_change end
item.options = mergeDeep(item.options, value.options ?? {})
item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
KiloAgent.processConfigItem(item) // kilocode_change - populate displayName from options
}
function referencePrompt(reference: KiloReference.Resolved) {
if (reference.kind === "local") {
return [
`You are configured reference @${reference.name}, a read-only research agent for external reference material.`,
`Local directory: ${reference.path}`,
`Inspect this directory as the primary reference source. Prefer repo_overview with path ${JSON.stringify(reference.path)} before broader searches. Do not edit files.`,
`Return exact absolute file paths for findings whenever possible.`,
].join("\n\n")
}
if (reference.kind === "invalid") {
return [
`You are configured reference @${reference.name}, but this reference is not usable yet.`,
`Configured repository: ${reference.repository}`,
`Problem: ${reference.message}`,
`Explain this configuration problem if invoked. Do not edit files or attempt fallback clones.`,
].join("\n\n")
}
function referencePrompt(reference: ReferenceV1.Resolved) {
if (reference.kind === "local") {
return [
`You are configured reference @${reference.name}, a read-only research agent for external reference material.`,
`Local directory: ${reference.path}`,
`Inspect this directory as the primary reference source. Prefer repo_overview with path ${JSON.stringify(reference.path)} before broader searches. Do not edit files.`,
`Repository: ${reference.repository}`,
...(reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
`Cached directory: ${reference.path}`,
`Kilo materializes this configured repository before use. Do not call repo_clone for this reference.`, // kilocode_change
`Inspect the cached directory as the primary reference source. Prefer repo_overview with path ${JSON.stringify(reference.path)} before broader searches, then use Glob, Grep, and Read inside that directory. Do not edit files.`,
`Return exact absolute file paths for findings whenever possible.`,
].join("\n\n")
}
if (reference.kind === "invalid") {
return [
`You are configured reference @${reference.name}, but this reference is not usable yet.`,
`Configured repository: ${reference.repository}`,
`Problem: ${reference.message}`,
`Explain this configuration problem if invoked. Do not edit files or attempt fallback clones.`,
].join("\n\n")
function referenceDescription(reference: KiloReference.Resolved) {
if (reference.kind === "local") return `Scout reference for local directory ${reference.path}`
if (reference.kind === "git") return `Scout reference for repository ${reference.repository}`
return `Invalid Scout reference for repository ${reference.repository}`
}
return [
`You are configured reference @${reference.name}, a read-only research agent for external reference material.`,
`Repository: ${reference.repository}`,
...(reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
`Cached directory: ${reference.path}`,
`Kilo materializes this configured repository before use. Do not call repo_clone for this reference.`, // kilocode_change
`Inspect the cached directory as the primary reference source. Prefer repo_overview with path ${JSON.stringify(reference.path)} before broader searches, then use Glob, Grep, and Read inside that directory. Do not edit files.`,
`Return exact absolute file paths for findings whenever possible.`,
].join("\n\n")
}
function referenceDescription(reference: ReferenceV1.Resolved) {
if (reference.kind === "local") return `Scout reference for local directory ${reference.path}`
if (reference.kind === "git") return `Scout reference for repository ${reference.repository}`
return `Invalid Scout reference for repository ${reference.repository}`
}
if (flags.experimentalScout) {
const resolvedReferences = ReferenceV1.resolveAll({
references: ConfigReference.normalize(cfg.reference ?? {}), // kilocode_change
directory: ctx.directory,
worktree: ctx.worktree,
})
for (const resolved of resolvedReferences) {
if (agents[resolved.name]) continue
const localPath = resolved.kind === "invalid" ? undefined : resolved.path
agents[resolved.name] = {
name: resolved.name,
description: referenceDescription(resolved),
permission: Permission.merge(
agents.scout.permission,
Permission.fromConfig({
repo_clone: "deny",
...(localPath
? {
external_directory: {
[localPath]: "allow",
[path.join(localPath, "*")]: "allow",
},
}
: {}),
}),
),
prompt: referencePrompt(resolved),
options: { reference: cfg.reference?.[resolved.name], resolved },
mode: "subagent",
native: false,
if (flags.experimentalScout) {
const resolvedReferences = KiloReference.resolveAll({
references: cfg.reference ?? {}, // kilocode_change
directory: ctx.directory,
worktree: ctx.worktree,
})
for (const resolved of resolvedReferences) {
if (agents[resolved.name]) continue
const localPath = resolved.kind === "invalid" ? undefined : resolved.path
agents[resolved.name] = {
name: resolved.name,
description: referenceDescription(resolved),
permission: Permission.merge(
agents.scout.permission,
Permission.fromConfig({
repo_clone: "deny",
...(localPath
? {
external_directory: {
[localPath]: "allow",
[path.join(localPath, "*")]: "allow",
},
}
: {}),
}),
),
prompt: referencePrompt(resolved),
options: { reference: cfg.reference?.[resolved.name], resolved },
mode: "subagent",
native: false,
}
}
}
}
// Ensure Truncate.GLOB is allowed unless explicitly configured
for (const name in agents) {
const agent = agents[name]
const explicit = agent.permission.some((r) => {
if (r.permission !== "external_directory") return false
if (r.action !== "deny") return false
return r.pattern === Truncate.GLOB
})
if (explicit) continue
// Ensure Truncate.GLOB is allowed unless explicitly configured
for (const name in agents) {
const agent = agents[name]
const explicit = agent.permission.some((r) => {
if (r.permission !== "external_directory") return false
if (r.action !== "deny") return false
return r.pattern === Truncate.GLOB
})
if (explicit) continue
agents[name].permission = Permission.merge(
agents[name].permission,
Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
)
}
KiloAgent.hardenSystemAgents(agents) // kilocode_change - keep system utility agents deny-only after config merges
const get = Effect.fnUntraced(function* (agent: string) {
return agents[KiloAgent.resolveKey(agent)] // kilocode_change - treat "build" as "code"
})
const list = Effect.fnUntraced(function* () {
const cfg = yield* config.get()
return pipe(
agents,
values(),
sortBy(
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "code"), "desc"], // kilocode_change - renamed from "build" to "code"
[(x) => x.name, "asc"],
),
)
})
const defaultInfo = Effect.fnUntraced(function* () {
const c = yield* config.get()
if (c.default_agent) {
// kilocode_change start
const effective = KiloAgent.resolveKey(c.default_agent)
const agent = agents[effective]
// kilocode_change end
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
return agent
agents[name].permission = Permission.merge(
agents[name].permission,
Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
)
}
// kilocode_change start - prefer "code" as default agent (key order changes after rename from "build")
const code = agents.code
if (code && code.mode !== "subagent" && code.hidden !== true) return code
// kilocode_change end
const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
if (!visible) throw new Error("no primary visible agent found")
return visible
})
const defaultAgent = Effect.fnUntraced(function* () {
return (yield* defaultInfo()).name
})
KiloAgent.hardenSystemAgents(agents) // kilocode_change - keep system utility agents deny-only after config merges
return {
version: KiloAgent.cacheKey(cfg), // kilocode_change
get,
list,
defaultInfo,
defaultAgent,
} satisfies State
}),
)
const get = Effect.fnUntraced(function* (agent: string) {
return agents[KiloAgent.resolveKey(agent)] // kilocode_change - treat "build" as "code"
})
const list = Effect.fnUntraced(function* () {
const cfg = yield* config.get()
return pipe(
agents,
values(),
sortBy(
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "code"), "desc"], // kilocode_change - renamed from "build" to "code"
[(x) => x.name, "asc"],
),
)
})
const defaultInfo = Effect.fnUntraced(function* () {
const c = yield* config.get()
if (c.default_agent) {
// kilocode_change start
const effective = KiloAgent.resolveKey(c.default_agent)
const agent = agents[effective]
// kilocode_change end
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
return agent
}
// kilocode_change start - prefer "code" as default agent (key order changes after rename from "build")
const code = agents.code
if (code && code.mode !== "subagent" && code.hidden !== true) return code
// kilocode_change end
const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
if (!visible) throw new Error("no primary visible agent found")
return visible
})
const defaultAgent = Effect.fnUntraced(function* () {
return (yield* defaultInfo()).name
})
return {
version: KiloAgent.cacheKey(cfg), // kilocode_change
get,
list,
defaultInfo,
defaultAgent,
} satisfies State
}),
)
// kilocode_change start - rebuild cached agents when permission-relevant config changes
const current = Effect.fnUntraced(function* <A>(select: (s: State) => Effect.Effect<A>) {
+2
View File
@@ -14,6 +14,7 @@ import { Identifier } from "@/id/id"
import { context as instanceContext, type InstanceContext } from "@/project/instance-context" // kilocode_change
import { InstanceRef } from "@/effect/instance-ref"
import { LocalContext } from "@/util/local-context" // kilocode_change
import { LayerNode } from "@opencode-ai/core/effect/layer-node" // kilocode_change
const log = Log.create({ service: "bus" })
@@ -193,6 +194,7 @@ export const layer = Layer.effect(
)
export const defaultLayer = layer
export const node = LayerNode.make(layer, []) // kilocode_change
const { runPromise, runSync } = makeRuntime(Service, layer)
+1
View File
@@ -167,6 +167,7 @@ export const TuiThreadCommand = cmd({
// kilocode_change end
const auth = KiloTuiThreadDaemon.workerAuth() // kilocode_change - protect TUI-owned HTTP routes from unauthenticated local callers
const worker = new Worker(file, {
preload: ["@opentui/solid/preload"], // kilocode_change - Bun workers do not inherit the parent preload
env: {
...process.env,
...auth.env, // kilocode_change
+15 -3
View File
@@ -55,6 +55,9 @@ import {
import { unique } from "remeda"
// kilocode_change end
import { withTransientReadRetry } from "@/util/effect-http-client"
import * as Log from "@opencode-ai/core/util/log" // kilocode_change
const log = Log.create({ service: "config" }) // kilocode_change
// Custom merge function that concatenates array fields instead of replacing them
// Keep remeda's deep conditional merge type out of hot config-loading paths; TS profiling showed it dominates here.
@@ -70,7 +73,7 @@ function mergeConfigConcatArrays(target: Info, source: Info): Info {
return merged
}
function normalizeLoadedConfig(data: unknown) {
function normalizeLoadedConfig(data: unknown, source: string) {
if (!isRecord(data)) return data
const copy = KilocodeConfig.retireIndexingFlag({ ...data }, source) // kilocode_change
const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy
@@ -78,6 +81,7 @@ function normalizeLoadedConfig(data: unknown) {
delete copy.theme
delete copy.keybinds
delete copy.tui
log.warn("tui keys in the main config are deprecated; move them to tui.json", { path: source }) // kilocode_change
return copy
}
@@ -307,7 +311,7 @@ export const layer = Layer.effect(
),
)
const parsed = ConfigParse.jsonc(expanded, source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed), source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed, source), source)
if (!("path" in options)) return data
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
@@ -1053,6 +1057,14 @@ export const defaultLayer = layer.pipe(
Layer.provide(FetchHttpClient.layer),
)
export const node = LayerNode.make(layer, [FSUtil.node, Auth.node, Account.node, Env.node, Npm.node, httpClient])
export const node = LayerNode.make(layer, [
FSUtil.node,
Auth.node,
Account.node,
Env.node,
Npm.node,
httpClient,
Git.node,
]) // kilocode_change
export * as Config from "./config"
+11 -4
View File
@@ -39,6 +39,7 @@ export type HostMetadata = {
export interface Interface {
readonly get: () => Effect.Effect<Resolved>
readonly info: () => Effect.Effect<Info> // kilocode_change - editable config for Kilo console
readonly pluginOrigins: () => Effect.Effect<ConfigPlugin.Origin[]>
readonly waitForDependencies: () => Effect.Effect<void>
}
@@ -242,14 +243,15 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
// kilocode_change start - inject Kilo default plugins to keep TUI aligned with server config
const defaults = KilocodeDefaultPlugins.apply(
{ plugin: result.plugin, plugin_origins: acc.plugin_origins },
{ plugin: result.plugin ? [...result.plugin] : undefined, plugin_origins: acc.plugin_origins },
{ disabled: Flag.KILO_DISABLE_DEFAULT_PLUGINS },
)
result.plugin = defaults.plugin
const config = { ...result, plugin: defaults.plugin }
// kilocode_change end
return {
config: result,
config,
info: { ...acc.result, plugin: defaults.plugin }, // kilocode_change - include applied Kilo defaults
pluginOrigins: defaults.plugin_origins ?? [], // kilocode_change - exclude builtins from dependency installation
dirs: result.plugin?.length ? dirs : [],
}
@@ -280,12 +282,13 @@ export const layer = Layer.effect(
)
const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config))
const info = Effect.fn("TuiConfig.info")(() => Effect.succeed(data.info)) // kilocode_change
const pluginOrigins = Effect.fn("TuiConfig.pluginOrigins")(() => Effect.succeed(data.pluginOrigins))
const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() =>
Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid),
)
return Service.of({ get, pluginOrigins, waitForDependencies })
return Service.of({ get, info, pluginOrigins, waitForDependencies }) // kilocode_change
}).pipe(Effect.withSpan("TuiConfig.layer")),
)
@@ -301,6 +304,10 @@ export async function get() {
return runPromise((svc) => svc.get())
}
export async function info() {
return runPromise((svc) => svc.info())
}
export async function pluginOrigins() {
return runPromise((svc) => svc.pluginOrigins())
}
@@ -32,6 +32,7 @@ import { Permission } from "@/permission"
import { withTimeout } from "@/util/timeout"
import { Snapshot } from "@/snapshot"
import { cumulativeSessionDiff } from "@/kilocode/session-portability/cumulative-diff"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
async function provide<R>(input: { directory: string; fn: () => R }): Promise<R> {
const { provide } = await import("@/kilocode/instance")
@@ -382,6 +383,8 @@ export namespace KiloSessions {
Layer.provide(Session.defaultLayer),
)
export const node = LayerNode.make(layer, [Bus.node, Config.node, Session.node])
export async function enableRemote() {
if (remote) return
if (ingestDisabled) return
@@ -18,6 +18,7 @@ import { MemoryService } from "@kilocode/kilo-memory/effect/service"
import { MemoryEvents } from "@/kilocode/memory/events"
import { installMemoryRuntime } from "@/kilocode/memory/runtime"
import { KiloToolRegistry } from "@/kilocode/tool/registry"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
const log = Log.create({ service: "kilocode-bootstrap" })
@@ -99,4 +100,14 @@ export namespace KilocodeBootstrap {
Bus.defaultLayer,
]),
)
const memory = LayerNode.make(MemoryService.layer, [])
export const node = LayerNode.make(layer, [
KiloSessions.node,
Session.node,
SessionSummary.node,
Provider.node,
memory,
Bus.node,
])
}
@@ -21,7 +21,7 @@ import fuzzysort from "fuzzysort"
import { Index, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { useTerminalDimensions } from "@opentui/solid"
import { SplitBorder } from "@tui/component/border"
import { SplitBorder } from "@tui/ui/border"
import { useBindings, useOpencodeModeStack } from "@tui/keymap"
import { selectedForeground, useTheme } from "@tui/context/theme"
+1 -1
View File
@@ -13,7 +13,7 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
import { type BoxRenderable, type MouseEvent, type TextareaRenderable } from "@opentui/core"
import { useRenderer } from "@opentui/solid"
import { useTheme } from "@tui/context/theme"
import { SplitBorder, EmptyBorder } from "@tui/component/border"
import { SplitBorder, EmptyBorder } from "@tui/ui/border"
import { useKV } from "@tui/context/kv"
import { Spinner } from "@tui/component/spinner"
import type { ChatMessage, TypingMember } from "./types"
@@ -8,7 +8,7 @@
import { createEffect, on } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { TextAttributes } from "@opentui/core"
import * as Clipboard from "@tui/util/clipboard"
import * as Clipboard from "@tui/clipboard"
import { useBindings } from "@tui/keymap"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
@@ -34,6 +34,7 @@ export { KiloTerminalTitle } from "./terminal-title"
// Hot reload TUI-local settings (keybinds/theme/ui) when changed from the Kilo Console.
// Called from the App body (below SDKProvider and the TuiConfig provider).
export { useTuiConfigHotReload } from "@/kilocode/cli/cmd/tui/context/tui-config-hot-reload"
export { KiloTuiConfig } from "@/kilocode/cli/cmd/tui/context/tui-config"
// ---------------------------------------------------------------------------
// Constants
@@ -6,14 +6,14 @@ import { MemoryDecisions } from "@kilocode/kilo-memory/decisions"
import { MemoryToken } from "@kilocode/kilo-memory/token"
import { Global } from "@opencode-ai/core/global"
import { createMemo, createResource, For, Match, Show, Switch } from "solid-js"
import { relativeTime } from "@/cli/cmd/tui/feature-plugins/session/util"
import { useProject } from "@/cli/cmd/tui/context/project"
import { useSDK } from "@/cli/cmd/tui/context/sdk"
import { useTheme } from "@/cli/cmd/tui/context/theme"
import { useTuiConfig } from "@/cli/cmd/tui/context/tui-config"
import { useBindings } from "@/cli/cmd/tui/keymap"
import { useDialog, type DialogContext } from "@/cli/cmd/tui/ui/dialog"
import { getScrollAcceleration } from "@/cli/cmd/tui/util/scroll"
import { relativeTime } from "@/kilocode/cli/cmd/tui/relative-time"
import { useProject } from "@tui/context/project"
import { useSDK } from "@tui/context/sdk"
import { useTheme } from "@tui/context/theme"
import { useTuiConfig } from "@tui/config"
import { useBindings } from "@tui/keymap"
import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { getScrollAcceleration } from "@tui/util/scroll"
import { route } from "@/kilocode/cli/cmd/tui/memory-command"
import { errorMessage } from "@/util/error"
@@ -163,9 +163,7 @@ export function DialogMemoryHelp(props: { reason?: string }) {
esc
</text>
</box>
<Show when={props.reason}>
{(reason) => <text fg={theme.error}>{reason()}</text>}
</Show>
<Show when={props.reason}>{(reason) => <text fg={theme.error}>{reason()}</text>}</Show>
<box gap={0}>
<For each={MEMORY_COMMAND_CATALOG}>
{(item) => (
@@ -232,8 +230,8 @@ function DialogMemoryStatus(props: { workspace?: string; directory?: string }) {
<box>
<text fg={theme.text}>Startup context</text>
<text fg={theme.textMuted}>
{item().state.autoInject ? "on" : "off"} · last injected{" "}
{fmt(item().state.stats.lastInjectedTokens)} tokens
{item().state.autoInject ? "on" : "off"} · last injected {fmt(item().state.stats.lastInjectedTokens)}{" "}
tokens
</text>
</box>
<MemorySourcesInfo sources={item().sources} />
@@ -9,7 +9,7 @@ import { useRoute } from "@tui/context/route"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
import { useTheme } from "@tui/context/theme"
import { useTuiConfig } from "@tui/context/tui-config"
import { useTuiConfig } from "@tui/config"
import { useToast } from "@tui/ui/toast"
import { getScrollAcceleration } from "@tui/util/scroll"
import { errorMessage } from "@/util/error"
@@ -1,7 +1,7 @@
import type { KiloClient } from "@kilocode/sdk/v2"
import type { CliRenderer } from "@opentui/core"
import type { DialogContext } from "@/cli/cmd/tui/ui/dialog"
import type { ToastContext } from "@/cli/cmd/tui/ui/toast"
import type { DialogContext } from "@tui/ui/dialog"
import type { ToastContext } from "@tui/ui/toast"
import {
showMemoryDialog,
showMemoryHelpDialog,
@@ -2,7 +2,7 @@ import type { TuiPluginApi } from "@kilocode/plugin/tui"
import { MemoryAutosaveStatus } from "@kilocode/kilo-memory/autosave-status"
import { createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
import * as Log from "@opencode-ai/core/util/log"
import { relativeTime } from "@/cli/cmd/tui/feature-plugins/session/util"
import { relativeTime } from "@/kilocode/cli/cmd/tui/relative-time"
import { route } from "@/kilocode/cli/cmd/tui/memory-command"
import { errorMessage } from "@/util/error"
import { Locale } from "@/util/locale"
@@ -39,12 +39,12 @@ export function MemorySidebar(props: { api: TuiPluginApi; sessionID: string }) {
const [data] = createResource(
() => `${workspace() ?? "__default__"}:${dir()}:${tick()}`,
async () => {
const status = await props.api.client.memory.status(route({ workspace: workspace(), directory: dir() })).catch(
(error: unknown) => {
const status = await props.api.client.memory
.status(route({ workspace: workspace(), directory: dir() }))
.catch((error: unknown) => {
log.warn("memory status unavailable", { error: errorMessage(error) })
return undefined
},
)
})
if (!status) return
if (status.error || !status.data) return
return status.data
@@ -9,9 +9,9 @@
* Kept separate from `tui-config.tsx` so the store factory has no SDK/event imports.
*/
import { onCleanup, onMount } from "solid-js"
import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { useSDK } from "@/cli/cmd/tui/context/sdk"
import { useEvent } from "@/cli/cmd/tui/context/event"
import type { TuiConfig } from "@tui/config"
import { useSDK } from "@tui/context/sdk"
import { useEvent } from "@tui/context/event"
import { KiloTuiConfig } from "./tui-config"
/**
@@ -7,15 +7,13 @@
*/
import { createContext, useContext, type ParentProps } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
import { KeymapLeaderTimeoutDefault } from "@/cli/cmd/tui/config/tui-schema"
import { LeaderTimeoutDefault, TuiConfig, TuiConfigProvider, useTuiConfig } from "@tui/config"
import { TuiKeybind } from "@tui/config/keybind"
import { createBindingLookup } from "@opentui/keymap/extras"
import { KiloTitleIcon } from "@/kilocode/cli/cmd/tui/title-icon"
export type SetTuiConfig = (next: TuiConfig.Info) => void
const ConfigContext = createContext<TuiConfig.Resolved>()
const SetContext = createContext<SetTuiConfig>()
export namespace KiloTuiConfig {
@@ -39,7 +37,8 @@ export namespace KiloTuiConfig {
commandMap: TuiKeybind.CommandMap,
bindingDefaults: TuiKeybind.bindingDefaults(),
}),
leader_timeout: next.leader_timeout ?? KeymapLeaderTimeoutDefault,
leader_timeout: next.leader_timeout ?? LeaderTimeoutDefault,
mouse: next.mouse ?? true,
}
if (JSON.stringify(config.keybinds.bindings) === JSON.stringify(store.keybinds.bindings)) {
config.keybinds = store.keybinds
@@ -52,16 +51,14 @@ export namespace KiloTuiConfig {
export function Provider(props: ParentProps<{ config: TuiConfig.Resolved }>) {
const store = makeStore(props.config)
return (
<ConfigContext.Provider value={store.config}>
<TuiConfigProvider config={store.config}>
<SetContext.Provider value={store.set}>{props.children}</SetContext.Provider>
</ConfigContext.Provider>
</TuiConfigProvider>
)
}
export function use() {
const value = useContext(ConfigContext)
if (!value) throw new Error("TuiConfig context must be used within a context provider")
return value
return useTuiConfig()
}
export function useSet() {
@@ -1,5 +1,5 @@
import type { PermissionRequest } from "@kilocode/sdk/v2"
import { useTheme } from "@/cli/cmd/tui/context/theme"
import { useTheme } from "@tui/context/theme"
import { MemoryPermissionRegistry } from "@/kilocode/cli/cmd/tui/routes/session/memory-permission"
function MemoryBody(props: { request: PermissionRequest }) {
@@ -0,0 +1,11 @@
export function relativeTime(timestamp: number) {
const seconds = Math.floor((Date.now() - timestamp) / 1000)
if (seconds < 60) return "just now"
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
if (days < 7) return `${days}d ago`
return new Date(timestamp).toLocaleDateString(undefined, { month: "short", day: "numeric" })
}
+2 -2
View File
@@ -3,8 +3,8 @@
// When upstream adds a new command to index.ts, add it here too.
import { AcpCommand } from "../cli/cmd/acp"
import { McpCommand } from "../cli/cmd/mcp"
import { TuiThreadCommand } from "../cli/cmd/tui/thread"
import { AttachCommand } from "../cli/cmd/tui/attach"
import { TuiThreadCommand } from "../cli/cmd/tui"
import { AttachCommand } from "../cli/cmd/attach"
import { RunCommand } from "../cli/cmd/run"
import { GenerateCommand } from "../cli/cmd/generate"
import { DebugCommand } from "../cli/cmd/debug"
@@ -12,7 +12,7 @@ import { useDialog } from "@tui/ui/dialog"
import { useSync } from "@tui/context/sync"
import { useToast } from "@tui/ui/toast"
import { Link } from "@tui/ui/link"
import * as Clipboard from "@tui/util/clipboard"
import * as Clipboard from "@tui/clipboard"
import { DialogKiloOrganization } from "./dialog-kilo-organization.js"
// These types are OpenCode-internal and imported at runtime
@@ -43,7 +43,7 @@ export function KiloAutoMethod(props: KiloAutoMethodProps) {
useKeyboard((evt: any) => {
if (evt.name === "c" && !evt.ctrl && !evt.meta) {
const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4}/)?.[0] ?? props.authorization.url
Clipboard.copy(code)
Clipboard.write(code)
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
.catch(toast.error)
}
@@ -1,5 +1,5 @@
import { createMemo, Match, Switch, type JSX } from "solid-js"
import { SplitBorder } from "@tui/component/border"
import { SplitBorder } from "@tui/ui/border"
import { useTheme } from "@tui/context/theme"
import { parseKiloErrorCode, kiloErrorTitle, kiloErrorDescription } from "@/kilocode/kilo-errors"
import type { AssistantMessage } from "@kilocode/sdk/v2"
@@ -1,6 +1,6 @@
import { Telemetry } from "@kilocode/kilo-telemetry"
import { Agent } from "@/agent/agent"
import { TuiEvent } from "@/cli/cmd/tui/event"
import { TuiEvent } from "@/server/tui-event"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Identifier } from "@/id/id"
@@ -1,5 +1,5 @@
import type { TuiPlugin } from "@kilocode/plugin/tui"
import type { InternalTuiPlugin } from "@/cli/cmd/tui/plugin/internal"
import type { InternalTuiPlugin } from "@/plugin/tui/internal"
import { DialogMemoryHelp } from "@/kilocode/cli/cmd/tui/component/dialog-memory"
const id = "internal:kilo-memory-palette"
@@ -1,5 +1,5 @@
import type { TuiPlugin } from "@kilocode/plugin/tui"
import type { InternalTuiPlugin } from "@/cli/cmd/tui/plugin/internal"
import type { InternalTuiPlugin } from "@/plugin/tui/internal"
import { MemoryPermission } from "@/kilocode/cli/cmd/tui/permissions"
const id = "internal:kilo-permissions"
@@ -2,7 +2,7 @@ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@kilocode/plugin/
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
import type { IndexingStatus, IndexingStatusState } from "@kilocode/kilo-indexing/status"
import * as Log from "@opencode-ai/core/util/log"
import { useSync } from "@/cli/cmd/tui/context/sync"
import { useSync } from "@tui/context/sync"
import { formatIndexingLabel } from "../indexing-label"
import { indexingEnabled } from "../indexing-feature"
@@ -1,5 +1,5 @@
import type { TuiPlugin } from "@kilocode/plugin/tui"
import type { InternalTuiPlugin } from "@/cli/cmd/tui/plugin/internal"
import type { InternalTuiPlugin } from "@/plugin/tui/internal"
import { MemorySidebar } from "@/kilocode/cli/cmd/tui/component/memory-sidebar"
const id = "internal:kilo-sidebar-memory"
@@ -0,0 +1,97 @@
import path from "path"
import { ConfigReference } from "@opencode-ai/core/config/reference"
import { Global } from "@opencode-ai/core/global"
import { parseRepositoryReference, repositoryCachePath, type RemoteReference } from "@/util/repository"
export type Resolved =
| {
name: string
kind: "local"
path: string
}
| {
name: string
kind: "git"
repository: string
reference: RemoteReference
path: string
branch?: string
}
| {
name: string
kind: "invalid"
repository?: string
message: string
}
type Normalized =
| { kind: "local"; path: string }
| { kind: "git"; repository: string; branch?: string }
| { kind: "invalid"; message: string }
function normalize(name: string, entry: ConfigReference.Entry): Normalized {
if (name.length === 0) return { kind: "invalid", message: "Reference alias must not be empty" }
if (/[\/\s`,]/.test(name)) {
return { kind: "invalid", message: "Reference alias must not contain /, whitespace, comma, or backtick" }
}
if (typeof entry === "string") {
if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
return { kind: "local", path: entry }
}
return { kind: "git", repository: entry }
}
if ("path" in entry) return { kind: "local", path: entry.path }
return { kind: "git", repository: entry.repository, branch: entry.branch }
}
function local(input: { directory: string; worktree: string; value: string }) {
if (input.value.startsWith("~/")) return path.join(Global.Path.home, input.value.slice(2))
if (path.isAbsolute(input.value)) return input.value
return path.resolve(input.worktree === "/" ? input.directory : input.worktree, input.value)
}
function resolve(name: string, entry: Normalized, directory: string, worktree: string): Resolved {
if (entry.kind === "invalid") return { name, kind: "invalid", message: entry.message }
if (entry.kind === "local") {
return { name, kind: "local", path: local({ directory, worktree, value: entry.path }) }
}
const reference = parseRepositoryReference(entry.repository)
if (!reference || reference.protocol === "file:") {
return {
name,
kind: "invalid",
repository: entry.repository,
message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand",
}
}
return {
name,
kind: "git",
repository: entry.repository,
reference,
path: repositoryCachePath(reference),
branch: entry.branch,
}
}
export function resolveAll(input: { references: ConfigReference.Info; directory: string; worktree: string }) {
const seen = new Map<string, { name: string; branch?: string }>()
return Object.entries(input.references).map(([name, entry]) => {
const item = resolve(name, normalize(name, entry), input.directory, input.worktree)
if (item.kind !== "git") return item
const existing = seen.get(item.path)
if (!existing) {
seen.set(item.path, { name, branch: item.branch })
return item
}
if (existing.branch === item.branch) return item
return {
name,
kind: "invalid" as const,
repository: item.repository,
message: `Reference conflicts with @${existing.name}: both use ${item.path}, but @${existing.name} requests ${existing.branch ?? "default branch"} and @${name} requests ${item.branch ?? "default branch"}`,
}
})
}
@@ -3,8 +3,8 @@
import type { SuggestionRequest } from "@kilocode/sdk/v2"
import { createMemo, createSignal, For } from "solid-js"
import { useSDK } from "../../../cli/cmd/tui/context/sdk"
import { selectedForeground, useTheme } from "../../../cli/cmd/tui/context/theme"
import { useSDK } from "@tui/context/sdk"
import { selectedForeground, useTheme } from "@tui/context/theme"
export function SuggestBar(props: { request: SuggestionRequest }) {
const sdk = useSDK()
@@ -2,12 +2,12 @@
import type { SuggestionRequest } from "@kilocode/sdk/v2"
import { createMemo, createSignal, For } from "solid-js"
import { SplitBorder } from "../../../cli/cmd/tui/component/border"
import { useSDK } from "../../../cli/cmd/tui/context/sdk"
import { useTuiConfig } from "../../../cli/cmd/tui/context/tui-config"
import { useBindings } from "../../../cli/cmd/tui/keymap"
import { tint, useTheme } from "../../../cli/cmd/tui/context/theme"
import { useDialog } from "../../../cli/cmd/tui/ui/dialog"
import { SplitBorder } from "@tui/ui/border"
import { useSDK } from "@tui/context/sdk"
import { useTuiConfig } from "@tui/config"
import { useBindings } from "@tui/keymap"
import { tint, useTheme } from "@tui/context/theme"
import { useDialog } from "@tui/ui/dialog"
// The footer-mounted overlay only ever hosts blocking suggestions now; the
// built-in suggest tool emits non-blocking requests that render inline at
@@ -1,7 +1,7 @@
/** @jsxImportSource @opentui/solid */
import { createMemo, Match, Show, Switch, type JSX } from "solid-js"
import { useTheme } from "../../../cli/cmd/tui/context/theme"
import { useTheme } from "@tui/context/theme"
import type { SuggestionRequest, ToolPart as MessageToolPart } from "@kilocode/sdk/v2"
import { SuggestBar } from "./bar"
+4 -5
View File
@@ -5,9 +5,8 @@ import { applyEdits, modify } from "jsonc-parser"
import { mergeDeep } from "remeda"
import { Global } from "@opencode-ai/core/global"
import { ConfigParse } from "@/config/parse"
import { CurrentWorkingDirectory } from "@/cli/cmd/tui/config/cwd"
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { TuiInfo } from "@/cli/cmd/tui/config/tui-schema"
import { CurrentWorkingDirectory } from "@/config/tui-cwd"
import { TuiConfig } from "@/config/tui"
import { KilocodeKeybinds } from "./keybinds"
import { Filesystem } from "@/util/filesystem"
import { isRecord } from "@/util/record"
@@ -18,7 +17,7 @@ export namespace KilocodeTuiConfig {
export const Scope = z.enum(["project", "global"])
export type Scope = z.infer<typeof Scope>
export const Patch = TuiInfo
export const Patch = TuiConfig.Info
export type Patch = Schema.Schema.Type<typeof Patch>
export type Editable = Omit<Patch, "keybinds"> & { keybinds?: Record<string, string> }
@@ -90,7 +89,7 @@ export namespace KilocodeTuiConfig {
function parse(input: string, file: string): Patch {
const data = ConfigParse.jsonc(input, file)
if (!isRecord(data)) return {}
return writable(ConfigParse.schema(TuiInfo, normalize(data), file))
return writable(ConfigParse.schema(TuiConfig.Info, normalize(data), file))
}
function normalize(raw: Record<string, unknown>) {
@@ -1,4 +1,4 @@
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
import { TuiKeybind } from "@opencode-ai/tui/config/keybind"
import { Schema } from "effect"
export namespace KilocodeKeybinds {
+2
View File
@@ -2,6 +2,7 @@ import type { ChildProcessWithoutNullStreams } from "child_process"
import path from "path"
import os from "os"
import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log" // kilocode_change
import { text } from "node:stream/consumers"
import fs from "fs/promises"
import { Filesystem } from "@/util/filesystem"
@@ -16,6 +17,7 @@ import { Npm } from "@opencode-ai/core/npm"
import { TsCheck } from "../kilocode/ts-check" // kilocode_change
import type { RuntimeFlags } from "@/effect/runtime-flags"
const log = Log.create({ service: "lsp.server" }) // kilocode_change
const pathExists = async (p: string) =>
fs
.stat(p)
@@ -1,8 +1,11 @@
import { createConnection } from "net"
import { createServer } from "http"
import * as Log from "@opencode-ai/core/util/log" // kilocode_change
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"
import * as KiloOAuthCallback from "../kilocode/mcp-oauth-callback" // kilocode_change
const log = Log.create({ service: "mcp.oauth-callback" }) // kilocode_change
// Current callback server configuration (may differ from defaults if custom redirectUri is used)
let currentPort = OAUTH_CALLBACK_PORT
let currentPath = OAUTH_CALLBACK_PATH
+1 -1
View File
@@ -497,6 +497,6 @@ export function toConfig(rules: Ruleset): ConfigPermissionV1.Info {
}
// kilocode_change end
export const node = LayerNode.make(layer, [EventV2Bridge.node])
export const node = LayerNode.make(layer, [EventV2Bridge.node, Config.node, Database.node]) // kilocode_change
export * as Permission from "."
+4 -3
View File
@@ -1,4 +1,5 @@
import type { Hooks, PluginInput } from "@kilocode/plugin"
import * as Log from "@opencode-ai/core/util/log" // kilocode_change
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { OAUTH_DUMMY_KEY } from "../../auth"
import os from "os"
@@ -7,6 +8,8 @@ import { createServer } from "http"
import { refreshCodexAuth } from "@/kilocode/provider/codex-refresh" // kilocode_change
import { OpenAIWebSocketPool } from "./ws-pool"
const log = Log.create({ service: "plugin.codex" }) // kilocode_change
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
const ISSUER = "https://auth.openai.com"
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
@@ -25,9 +28,7 @@ const ALLOWED_MODELS = new Set([
// kilocode_change end
])
// kilocode_change start
const DISALLOWED_MODELS = new Set([
"gpt-5.5-pro",
])
const DISALLOWED_MODELS = new Set(["gpt-5.5-pro"])
// kilocode_change end
interface PkceCodes {
@@ -1,10 +1,13 @@
import WebSocket from "ws"
import * as Log from "@opencode-ai/core/util/log" // kilocode_change
import { ProviderError } from "@/provider/error"
import { isRecord } from "@/util/record"
import { OpenAIWebSocket } from "./ws"
export const TITLE_HEADER = "x-kilo-title"
const log = Log.create({ service: "plugin.openai.ws" }) // kilocode_change
export interface CreateWebSocketFetchOptions {
httpFetch?: typeof globalThis.fetch
url?: string
@@ -78,6 +78,7 @@ export const node = LayerNode.make(layer, [
LSP.node,
Plugin.node,
Project.node,
KilocodeBootstrap.node, // kilocode_change
Snapshot.node,
Vcs.node,
])
+1 -1
View File
@@ -261,6 +261,6 @@ export const defaultLayer = Layer.suspend(() =>
)
// kilocode_change end
export const node = LayerNode.make(layer, [Auth.node, Plugin.node])
export const node = LayerNode.make(layer, [Auth.node, Plugin.node, ModelCache.node]) // kilocode_change
export * as ProviderAuth from "./auth"
@@ -6,6 +6,8 @@ import { Config } from "../config/config"
import { Auth } from "../auth"
import type { Provider } from "@opencode-ai/core/models-dev"
import * as Log from "@opencode-ai/core/util/log"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
type Models = Provider["models"]
type KiloOptions = NonNullable<Parameters<typeof fetchKiloModels>[0]>
@@ -282,4 +284,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(kiloModelsLayer),
)
const kiloModels = LayerNode.make(kiloModelsLayer, [])
export const node = LayerNode.make(layer, [Auth.node, Config.node, kiloModels, httpClient])
export * as ModelCache from "./model-cache"
+3
View File
@@ -6,6 +6,7 @@ import * as Core from "@opencode-ai/core/models-dev"
import { Context, Effect, Layer } from "effect"
import { AI_SDK_PROVIDERS, KILO_OPENROUTER_BASE, PROMPTS } from "@kilocode/kilo-gateway"
import { overlay } from "@/kilocode/anaconda-desktop/provider"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
export const Model = Core.Model
export type Model = Core.Model
@@ -105,5 +106,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(ModelCache.defaultLayer),
)
export const node = LayerNode.make(layer, [Core.node, Config.node, Auth.node, ModelCache.node])
export { AI_SDK_PROVIDERS, PROMPTS }
export * as ModelsDev from "./models"
@@ -728,6 +728,7 @@ export const node = LayerNode.make(layer, [
Provider.node,
EventV2Bridge.node,
RuntimeFlags.node,
Database.node, // kilocode_change
])
export * as SessionCompaction from "./compaction"
+4
View File
@@ -4,6 +4,7 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Provider } from "@/provider/provider"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Log } from "@opencode-ai/core/util/log" // kilocode_change
import { Context, Effect, Layer } from "effect"
import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool } from "ai"
@@ -39,6 +40,8 @@ import { LLMAISDK } from "./llm/ai-sdk"
import { LLMNativeRuntime } from "./llm/native-runtime"
import { LLMRequestPrep } from "./llm/request"
const log = Log.create({ service: "llm" }) // kilocode_change
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
export type StreamInput = {
@@ -94,6 +97,7 @@ const live: Layer.Layer<
const flags = yield* RuntimeFlags.Service
const run = Effect.fn("LLM.run")(function* (input: StreamRequest) {
const l = log.clone().tag("providerID", input.model.providerID).tag("modelID", input.model.id) // kilocode_change
yield* Effect.logInfo("stream", {
providerID: input.model.providerID,
modelID: input.model.id,
+5 -1
View File
@@ -2052,7 +2052,10 @@ export const PromptInput = Schema.Struct({
// `parts` type from the exported Schema input types so callers see a proper
// tagged union.
type PartInputUnion =
MessageV2.TextPartInput | MessageV2.FilePartInput | MessageV2.AgentPartInput | MessageV2.SubtaskPartInput
| MessageV2.TextPartInput
| MessageV2.FilePartInput
| MessageV2.AgentPartInput
| MessageV2.SubtaskPartInput
export type PromptInput = Omit<Schema.Schema.Type<typeof PromptInput>, "parts" | "editorContext"> & {
parts: PartInputUnion[]
editorContext?: MessageV2.EditorContext
@@ -2169,6 +2172,7 @@ export const node = LayerNode.make(layer, [
EventV2Bridge.node,
RuntimeFlags.node,
Database.node,
Question.node, // kilocode_change
])
export * as SessionPrompt from "./prompt"
+7 -1
View File
@@ -195,6 +195,12 @@ export const DiffInput = Schema.Struct({
})
export type DiffInput = Schema.Schema.Type<typeof DiffInput>
export const node = LayerNode.make(layer, [Session.node, Snapshot.node, EventV2Bridge.node, Config.node])
export const node = LayerNode.make(layer, [
Session.node,
Snapshot.node,
EventV2Bridge.node,
Config.node,
Storage.node, // kilocode_change
])
export * as SessionSummary from "./summary"
+1 -1
View File
@@ -54,6 +54,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make(layer, [Config.node, Session.node, ShareNext.node, RuntimeFlags.node])
export const node = LayerNode.make(layer, [Config.node, Session.node, RuntimeFlags.node]) // kilocode_change
export * as SessionShare from "./session"
+2 -3
View File
@@ -124,9 +124,7 @@ const add = Effect.fnUntraced(function* (state: State, match: Match, events: Eve
}).pipe(
Effect.catch(
Effect.fnUntraced(function* (err) {
const message = FrontmatterError.isInstance(err)
? err.data.message
: `Failed to parse skill ${match.path}` // kilocode_change
const message = FrontmatterError.isInstance(err) ? err.data.message : `Failed to parse skill ${match.path}` // kilocode_change
const { Session } = yield* Effect.promise(() => import("@/session/session"))
yield* events.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
yield* Effect.logError("failed to load skill", { skill: match.path, error: err }) // kilocode_change
@@ -418,6 +416,7 @@ export const node = LayerNode.make(layer, [
FSUtil.node,
Global.node,
RuntimeFlags.node,
Git.node, // kilocode_change
])
export * as Skill from "."
+2 -2
View File
@@ -524,7 +524,7 @@ export const layer: Layer.Layer<Service, never, Requirements> =
const clash = (a: string, b: string) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)
for (let i = 0; i < ops.length;) {
for (let i = 0; i < ops.length; ) {
const first = ops[i]!
const run = [first]
let j = i + 1
@@ -960,6 +960,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(EffectFlock.defaultLayer), // kilocode_change
)
export const node = LayerNode.make(layer, [FSUtil.node, AppProcess.node, Config.node])
export const node = LayerNode.make(layer, [FSUtil.node, AppProcess.node, Config.node, EffectFlock.node]) // kilocode_change
export * as Snapshot from "."
+8
View File
@@ -64,6 +64,9 @@ import * as ToolNetwork from "@/kilocode/sandbox/network" // kilocode_change
import { MemoryService } from "@kilocode/kilo-memory/effect/service" // kilocode_change
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { RepositoryCache } from "@opencode-ai/core/repository-cache" // kilocode_change
import { RipgrepBinary } from "@opencode-ai/core/ripgrep/binary" // kilocode_change
import { AppProcess } from "@opencode-ai/core/process" // kilocode_change
export function webSearchEnabled(
providerID: ProviderV2.ID,
@@ -394,6 +397,8 @@ export const defaultLayer: Layer.Layer<Service> = Layer.suspend(
// kilocode_change start
Layer.provide(
Ripgrep.layer.pipe(
Layer.provide(RipgrepBinary.layer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(ToolNetwork.httpLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
@@ -408,6 +413,7 @@ export const defaultLayer: Layer.Layer<Service> = Layer.suspend(
Layer.provide(Database.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
Layer.provide(SessionStatus.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer),
Layer.provide(Truncate.defaultLayer), // kilocode_change - split the pipe to stay within Effect's overload limit
)
.pipe(Layer.provide(Auth.defaultLayer)),
@@ -494,6 +500,7 @@ function isJsonSchemaObject(value: unknown): value is Record<string, unknown> {
const networkNode = LayerNode.make(ToolNetwork.httpLayer, [])
const busNode = LayerNode.make(Bus.layer, [])
const notebookNode = LayerNode.make(Notebook.defaultLayer, [])
const repositoryCacheNode = LayerNode.make(RepositoryCache.defaultLayer, [])
export const node = LayerNode.make(layer.pipe(Layer.provide(Ripgrep.defaultLayer)), [
Config.node,
@@ -521,6 +528,7 @@ export const node = LayerNode.make(layer.pipe(Layer.provide(Ripgrep.defaultLayer
Auth.node,
SessionStatus.node,
notebookNode,
repositoryCacheNode,
])
// kilocode_change end
+5 -4
View File
@@ -1,8 +1,9 @@
import { Effect, Schema } from "effect"
import DESCRIPTION from "./repo_clone.txt"
import * as Tool from "./tool"
import { repositoryCachePath } from "@/util/repository"
import { RepositoryCache } from "@/reference/repository-cache"
import { Global } from "@opencode-ai/core/global" // kilocode_change
import { Repository } from "@opencode-ai/core/repository" // kilocode_change
import { RepositoryCache } from "@opencode-ai/core/repository-cache" // kilocode_change
export const Parameters = Schema.Struct({
repository: Schema.String.annotate({
@@ -36,12 +37,12 @@ export const RepoCloneTool = Tool.define<typeof Parameters, Metadata, Repository
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
Effect.gen(function* () {
const reference = yield* RepositoryCache.parseRemoteReference(params.repository)
const reference = yield* RepositoryCache.parseRemote(params.repository)
if (params.branch) yield* RepositoryCache.validateBranch(params.branch)
const repository = reference.label
const remote = reference.remote
const localPath = repositoryCachePath(reference)
const localPath = Repository.cachePath(Global.Path.repos, reference)
yield* ctx.ask({
permission: "repo_clone",
+1 -1
View File
@@ -4,7 +4,7 @@ import { WarpGrepClient } from "@morphllm/morphsdk/tools/warp-grep/client" // ki
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
import { Instance } from "../kilocode/instance" // kilocode_change
import { EventV2Bridge } from "@/event-v2-bridge" // kilocode_change
import { TuiEvent } from "../cli/cmd/tui/event" // kilocode_change
import { TuiEvent } from "@/server/tui-event" // kilocode_change
import DESCRIPTION from "./warpgrep.txt"
// FREE_PERIOD_TODO: Remove KILO_WARPGREP_PROXY_URL constant and the proxy
@@ -1,6 +1,6 @@
// kilocode_change - new file
import { describe, expect, it } from "bun:test"
import { formatMarkdownTables } from "../../../src/cli/cmd/tui/util/markdown"
import { formatMarkdownTables } from "@tui/util/markdown"
describe("formatMarkdownTables", () => {
it("formats a simple table with fixed-width columns", () => {
+2 -3
View File
@@ -1,5 +1,5 @@
import { $ } from "bun"
import * as Observability from "@opencode-ai/core/effect/observability"
import * as Observability from "@opencode-ai/core/observability"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import * as fs from "fs/promises"
import os from "os"
@@ -24,8 +24,7 @@ import { remove as cleanup } from "../kilocode/cleanup" // kilocode_change
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
export const testInstanceStoreLayer = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
const makeTestRuntime = () =>
ManagedRuntime.make(testInstanceStoreLayer.pipe(Layer.provideMerge(Observability.layer)))
const makeTestRuntime = () => ManagedRuntime.make(testInstanceStoreLayer.pipe(Layer.provideMerge(Observability.layer)))
let testRuntime: ReturnType<typeof makeTestRuntime> | undefined
const runtime = () => (testRuntime ??= makeTestRuntime())
@@ -6,8 +6,8 @@
import { describe, expect, test } from "bun:test"
import { createEffect, createRoot } from "solid-js"
import { createBindingLookup } from "@opentui/keymap/extras"
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { TuiKeybind } from "@tui/config/keybind"
import { TuiConfig } from "@tui/config"
import { KiloTuiConfig } from "@/kilocode/cli/cmd/tui/context/tui-config"
import { KiloTerminalTitle } from "@/kilocode/cli/cmd/tui/terminal-title"
@@ -33,6 +33,7 @@ function resolve(input: TuiConfig.Info): TuiConfig.Resolved {
bindingDefaults: TuiKeybind.bindingDefaults(),
}),
leader_timeout: input.leader_timeout ?? 1_000,
mouse: input.mouse ?? true,
}
}
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { tmpdir } from "../../../fixture/fixture"
import { resolveThreadDirectory } from "../../../../src/cli/cmd/tui/thread"
import { resolveThreadDirectory } from "../../../../src/cli/cmd/tui"
import { KiloTuiThreadDaemon } from "../../../../src/kilocode/cli/cmd/tui/thread"
import { DaemonClient } from "../../../../src/kilocode/daemon/client"
@@ -106,12 +106,12 @@ describe("kilo tui thread", () => {
},
}),
}))
mock.module("@/cli/cmd/tui/validate-session", () => ({
mock.module("@/cli/tui/validate-session", () => ({
validateSession: async (input: { sessionID?: string }) => {
seen.push(input.sessionID ?? "")
},
}))
mock.module("@/cli/cmd/tui/config/tui", () => ({
mock.module("@/config/tui", () => ({
TuiConfig: {
get: async () => ({}),
},
@@ -23,7 +23,6 @@ import { MessageV2 } from "../../src/session/message-v2"
import * as SessionProcessorModule from "../../src/session/processor"
import { Session as SessionNs } from "../../src/session/session"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { Reference } from "../../src/reference/reference"
import { SessionCompaction } from "../../src/session/compaction"
import { SessionStatus } from "../../src/session/status"
import { SessionSummary } from "../../src/session/summary"
@@ -178,11 +177,9 @@ function runtime(layer: Layer.Layer<LLM.Service>, config = Config.defaultLayer)
Layer.provide(config),
Layer.provide(RuntimeFlags.layer()),
Layer.provide(scope),
Layer.provide(Reference.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(Reference.defaultLayer),
),
)
}
@@ -7,28 +7,14 @@ import { NodeFileSystem } from "@effect/platform-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Reference } from "../../src/reference/reference"
import { Instruction } from "../../src/session/instruction"
import { Global } from "@opencode-ai/core/global"
import { TestConfig } from "../fixture/config"
import { provideInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const reference = Layer.mock(Reference.Service)({
init: () => Effect.void,
list: () => Effect.succeed([]),
get: () => Effect.succeed(undefined),
ensure: () => Effect.void,
contains: () => Effect.succeed(false),
})
const it = testEffect(
Layer.mergeAll(
CrossSpawnSpawner.defaultLayer,
NodeFileSystem.layer,
reference,
RuntimeFlags.layer(),
testInstanceStoreLayer,
),
Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, RuntimeFlags.layer(), testInstanceStoreLayer),
)
const configLayer = TestConfig.layer()
@@ -5,7 +5,7 @@ import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log"
import { Agent } from "../../src/agent/agent"
import { GlobalBus } from "../../src/bus/global"
import { TuiEvent } from "../../src/cli/cmd/tui/event"
import { TuiEvent } from "../../src/server/tui-event"
import { Identifier } from "../../src/id/id"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
@@ -68,16 +68,11 @@ const todo = {
const session = makeRuntime(Session.Service, Session.defaultLayer)
const store = {
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
session.runPromise((svc) => svc.create(input)),
get: (id: SessionID) =>
session.runPromise((svc) => svc.get(id)),
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
session.runPromise((svc) => svc.messages(input)),
updateMessage: <T extends MessageV2.Info>(msg: T) =>
session.runPromise((svc) => svc.updateMessage(msg)),
updatePart: <T extends MessageV2.Part>(part: T) =>
session.runPromise((svc) => svc.updatePart(part)),
create: (input?: Parameters<Session.Interface["create"]>[0]) => session.runPromise((svc) => svc.create(input)),
get: (id: SessionID) => session.runPromise((svc) => svc.get(id)),
messages: (input: Parameters<Session.Interface["messages"]>[0]) => session.runPromise((svc) => svc.messages(input)),
updateMessage: <T extends MessageV2.Info>(msg: T) => session.runPromise((svc) => svc.updateMessage(msg)),
updatePart: <T extends MessageV2.Part>(part: T) => session.runPromise((svc) => svc.updatePart(part)),
}
const model = {
@@ -901,7 +896,9 @@ describe("plan follow-up", () => {
test("ask - falls back to configured code model when saved CLI code model is unavailable", () =>
withInstance(async () => {
await writeState({ model: { code: { providerID: ProviderV2.ID.make("missing"), modelID: ModelV2.ID.make("ghost") } } })
await writeState({
model: { code: { providerID: ProviderV2.ID.make("missing"), modelID: ModelV2.ID.make("ghost") } },
})
const get = spyOn(PlanFollowupRuntime, "agent").mockImplementation(async (name: string) => {
if (name === "code") {
return {
@@ -18,7 +18,7 @@ import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { Env } from "../../src/env"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Format } from "../../src/format"
import { Git } from "../../src/git"
@@ -33,8 +33,7 @@ import { Provider as ProviderSvc } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Question } from "../../src/question"
import { Reference } from "../../src/reference/reference"
import { RepositoryCache } from "../../src/reference/repository-cache"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Session } from "../../src/session/session"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
@@ -154,7 +153,6 @@ function makeHttp() {
SyncEvent.defaultLayer,
EventV2Bridge.defaultLayer,
Database.defaultLayer,
Reference.defaultLayer,
status,
MemoryService.layer,
).pipe(Layer.provideMerge(infra))
@@ -208,7 +206,6 @@ function makeHttp() {
Bus.layer,
infra,
Storage.defaultLayer,
Reference.defaultLayer,
),
),
)
@@ -22,7 +22,6 @@ import { KiloCompactionChunks } from "../../src/kilocode/session/compaction-chun
import { KiloSessionCompaction } from "../../src/kilocode/session/compaction"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
import { Reference } from "../../src/reference/reference"
import { SessionCompaction } from "../../src/session/compaction"
import * as SessionProcessorModule from "../../src/session/processor"
import type { SessionProcessor } from "../../src/session/processor"
@@ -40,6 +39,9 @@ import { remove as cleanup } from "./cleanup"
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("test-model")
const ref = { providerID, modelID }
const agents = Layer.mock(Agent.Service)({
get: () => Effect.succeed({ name: "compaction", mode: "primary", permission: [], options: {} } satisfies Agent.Info),
})
const previous = Flag.KILO_DB
const dbfile = path.join(os.tmpdir(), `kilo-compaction-chunks-${process.pid}-${crypto.randomUUID()}.db`)
@@ -227,13 +229,12 @@ function fakeRuntime(outputTokenMax?: number, error?: MessageV2.Assistant["error
Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus).pipe(
Layer.provide(ProviderTest.fake({ model }).layer),
Layer.provide(SessionNs.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(agents),
Layer.provide(Plugin.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(RuntimeFlags.layer({ outputTokenMax })),
Layer.provide(Reference.defaultLayer),
Layer.provide(bus),
Layer.provide(
Layer.mock(Config.Service)({
@@ -309,7 +310,6 @@ function liveRuntime(layer: Layer.Layer<LLM.Service>, context = 10_000) {
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(RuntimeFlags.layer()),
Layer.provide(Reference.defaultLayer),
Layer.provide(status),
Layer.provide(bus),
Layer.provide(
@@ -16,7 +16,6 @@ import { Plugin } from "../../src/plugin"
import type { Provider } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Reference } from "../../src/reference/reference"
import { Session } from "../../src/session/session"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
@@ -100,13 +99,6 @@ const llm = Layer.unwrap(
}),
)
const reference = Layer.mock(Reference.Service)({
init: () => Effect.void,
list: () => Effect.succeed([]),
get: () => Effect.succeed(undefined),
ensure: () => Effect.void,
contains: () => Effect.succeed(false),
})
const status = Layer.mergeAll(SessionStatus.defaultLayer, Bus.layer)
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
const deps = Layer.mergeAll(
@@ -117,7 +109,6 @@ const deps = Layer.mergeAll(
Plugin.defaultLayer,
Config.defaultLayer,
RuntimeFlags.layer(),
reference,
SessionSummary.defaultLayer,
Image.defaultLayer,
SyncEvent.defaultLayer,
@@ -126,7 +117,7 @@ const deps = Layer.mergeAll(
status,
llm,
).pipe(Layer.provideMerge(infra))
const env = SessionProcessor.layer.pipe(Layer.provideMerge(deps), Layer.provide(reference))
const env = SessionProcessor.layer.pipe(Layer.provideMerge(deps))
const it = testEffect(env)
@@ -489,9 +480,7 @@ describe("session processor empty tool-calls", () => {
LLMEvent.stepFinish({ index: 0, reason: "stop", usage: usage() }),
LLMEvent.finish({ reason: "stop", usage: usage() }),
).pipe(
Stream.tap((event) =>
event.type === "step-finish" ? state.session.remove(state.chat.id) : Effect.void,
),
Stream.tap((event) => (event.type === "step-finish" ? state.session.remove(state.chat.id) : Effect.void)),
),
)
const result = yield* state.handle.process(state.input)
@@ -16,7 +16,6 @@ import { Plugin } from "../../src/plugin"
import type { Provider } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Reference } from "../../src/reference/reference"
import { Session } from "../../src/session/session"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
@@ -98,13 +97,6 @@ const llm = Layer.unwrap(
}),
)
const reference = Layer.mock(Reference.Service)({
init: () => Effect.void,
list: () => Effect.succeed([]),
get: () => Effect.succeed(undefined),
ensure: () => Effect.void,
contains: () => Effect.succeed(false),
})
const status = Layer.mergeAll(SessionStatus.defaultLayer, Bus.layer)
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
const deps = Layer.mergeAll(
@@ -115,7 +107,6 @@ const deps = Layer.mergeAll(
Plugin.defaultLayer,
Config.defaultLayer,
RuntimeFlags.layer(),
reference,
SessionSummary.defaultLayer,
Image.defaultLayer,
SyncEvent.defaultLayer,
@@ -124,7 +115,7 @@ const deps = Layer.mergeAll(
status,
llm,
).pipe(Layer.provideMerge(infra))
const env = SessionProcessor.layer.pipe(Layer.provideMerge(deps), Layer.provide(reference))
const env = SessionProcessor.layer.pipe(Layer.provideMerge(deps))
const it = testEffect(env)
@@ -23,7 +23,6 @@ import { Plugin } from "../../src/plugin"
import type { Provider } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Reference } from "../../src/reference/reference"
import { Session } from "../../src/session/session"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
@@ -111,13 +110,6 @@ const llm = Layer.unwrap(
}),
)
const reference = Layer.mock(Reference.Service)({
init: () => Effect.void,
list: () => Effect.succeed([]),
get: () => Effect.succeed(undefined),
ensure: () => Effect.void,
contains: () => Effect.succeed(false),
})
const status = Layer.mergeAll(SessionStatus.defaultLayer, Bus.layer)
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
const deps = Layer.mergeAll(
@@ -128,7 +120,6 @@ const deps = Layer.mergeAll(
Plugin.defaultLayer,
Config.defaultLayer,
RuntimeFlags.layer(),
reference,
SessionSummary.defaultLayer,
Image.defaultLayer,
SyncEvent.defaultLayer,
@@ -137,7 +128,7 @@ const deps = Layer.mergeAll(
status,
llm,
).pipe(Layer.provideMerge(infra))
const env = SessionProcessor.layer.pipe(Layer.provideMerge(deps), Layer.provide(reference))
const env = SessionProcessor.layer.pipe(Layer.provideMerge(deps))
const it = testEffect(env)
@@ -17,7 +17,7 @@ import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { Env } from "../../src/env"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Format } from "../../src/format"
import { Git } from "../../src/git"
@@ -30,8 +30,7 @@ import { Provider as ProviderSvc } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Question } from "../../src/question"
import { Reference } from "../../src/reference/reference"
import { RepositoryCache } from "../../src/reference/repository-cache"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Session } from "../../src/session/session"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
@@ -144,7 +143,6 @@ function makeHttp() {
lsp,
mcp,
FSUtil.defaultLayer,
Reference.defaultLayer,
SyncEvent.defaultLayer,
EventV2Bridge.defaultLayer,
Database.defaultLayer,
@@ -161,7 +159,6 @@ function makeHttp() {
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Format.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Reference.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(Auth.defaultLayer), // kilocode_change
Layer.provideMerge(todo),
@@ -202,7 +199,6 @@ function makeHttp() {
Bus.layer,
infra,
Storage.defaultLayer,
Reference.defaultLayer,
),
),
)
@@ -25,8 +25,7 @@ import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider/provider"
import { Question } from "../../src/question"
import { Reference } from "../../src/reference/reference"
import { RepositoryCache } from "../../src/reference/repository-cache"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
import { LLM } from "../../src/session/llm"
@@ -43,7 +42,7 @@ import { Skill } from "../../src/skill"
import { Snapshot } from "../../src/snapshot"
import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate"
import { KiloHeadless } from "../../src/kilocode/permission/headless"
@@ -139,7 +138,6 @@ function makeHttp() {
lsp,
mcp,
FSUtil.defaultLayer,
Reference.defaultLayer,
SyncEvent.defaultLayer,
EventV2Bridge.defaultLayer,
Database.defaultLayer,
@@ -156,7 +154,6 @@ function makeHttp() {
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(Format.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Reference.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(Auth.defaultLayer), // kilocode_change
Layer.provideMerge(todo),
@@ -197,7 +194,6 @@ function makeHttp() {
Bus.layer,
infra,
Storage.defaultLayer,
Reference.defaultLayer,
),
),
)
@@ -341,8 +337,8 @@ it.live("headless run: subagent permission asks fail instead of waiting forever"
expect(err).toBeInstanceOf(Permission.DeniedError)
expect(yield* permission.list()).toEqual([])
expect(yield* KiloHeadless.denies(child.id)).toBe(true)
expect(yield* KiloHeadless.denies(root.id)).toBe(false)
expect(yield* KiloHeadless.denies(child.id)).toBe(true)
expect(yield* KiloHeadless.denies(root.id)).toBe(false)
KiloHeadless.clear(root.id)
}),
@@ -10,7 +10,7 @@ import { Global } from "@opencode-ai/core/global"
import { MessageID, SessionID } from "../../../src/session/schema"
import { Truncate } from "../../../src/tool/truncate"
import { RepoCloneTool } from "../../../src/tool/repo_clone"
import { RepositoryCache } from "../../../src/reference/repository-cache"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { disposeAllInstances, provideTmpdirInstance, tmpdirScoped } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
@@ -1,8 +1,8 @@
/** @jsxImportSource @opentui/solid */
import { describe, expect, test } from "bun:test"
import type { BackgroundProcessInfo, GlobalEvent } from "@kilocode/sdk/v2"
import { normalizeSyncEvent } from "../../src/cli/cmd/tui/context/event"
import { mount, wait } from "../cli/cmd/tui/sync-fixture"
import { normalizeSyncEvent } from "@tui/context/event"
import { mount, wait } from "../../../tui/test/cli/cmd/tui/sync-fixture"
function processInfo(id: string, sessionID: string, lifetime: BackgroundProcessInfo["lifetime"], updated: number) {
return {
@@ -80,7 +80,7 @@ describe("TUI sync event wire format", () => {
try {
emit({
directory: "/tmp/opencode/packages/opencode",
project: "proj_test",
project: "proj_other",
payload: {
type: "background_process.updated",
properties: {
@@ -1,6 +1,5 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { createLeadingTrailingSignal } from "@tui/feature-plugins/session/preview-pane"
import { createDebouncedSignal } from "@tui/util/signal"
describe("TUI scheduling", () => {
@@ -17,24 +16,4 @@ describe("TUI scheduling", () => {
dispose()
})
})
test("updates on the leading and trailing edges", async () => {
await createRoot(async (dispose) => {
const [value, , schedule] = createLeadingTrailingSignal("initial", 10)
schedule("leading")
expect(value()).toBe("leading")
schedule("middle")
schedule("trailing")
expect(value()).toBe("leading")
await Bun.sleep(30)
expect(value()).toBe("trailing")
schedule("next")
expect(value()).toBe("next")
dispose()
})
})
})
+2 -5
View File
@@ -6,7 +6,6 @@ import * as TestClock from "effect/testing/TestClock"
import * as TestConsole from "effect/testing/TestConsole"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
import type { Config } from "@/config/config"
import { Reference } from "@/reference/reference" // kilocode_change
import { TestInstance, withTmpdirInstance } from "../fixture/fixture"
import { InstanceStore } from "@/project/instance-store"
@@ -138,10 +137,8 @@ const liveEnv = TestConsole.layer
export const it = make<never, never>(testEnv, liveEnv)
// kilocode_change start
export const testEffect = <R, E>(layer: Layer.Layer<R, E>) => {
const full = Layer.merge(layer, Reference.defaultLayer)
return make(Layer.provideMerge(full, testEnv), Layer.provideMerge(full, liveEnv))
}
export const testEffect = <R, E>(layer: Layer.Layer<R, E>) =>
make<R, E>(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv))
export const testEffectBare = <R, E>(layer: Layer.Layer<R, E>) =>
make<R, E>(Layer.provideMerge(layer, testEnv), Layer.provideMerge(layer, liveEnv))
// kilocode_change end
+1
View File
@@ -3,6 +3,7 @@
import os from "os"
import path from "path"
import fs from "fs/promises"
import { setTimeout as sleep } from "node:timers/promises"
import { afterAll } from "bun:test"
import { remove as cleanup } from "./kilocode/cleanup" // kilocode_change
@@ -259,7 +259,7 @@ describe("event HttpApi", () => {
timestamp,
messageID: SessionMessageID.ID.create(),
delivery: "queue",
prompt: new Prompt({ text: "hello", files: [], agents: [], references: [] }), // kilocode_change - upstream made prompt a Prompt class
prompt: new Prompt({ text: "hello", files: [], agents: [] }), // kilocode_change - upstream made prompt a Prompt class
})
expect(properties(yield* Fiber.join(prompted))).toMatchObject({
timestamp: 1_234,
@@ -187,15 +187,15 @@ const env = LayerNode.buildLayer(LayerNode.group([root, LayerNode.make(TestLLMSe
const it = testEffect(env)
// kilocode_change start - exercise non-default output token ceilings in the processor
const capped = testEffect(
Layer.mergeAll(
TestLLMServer.layer,
SessionProcessor.layer.pipe(
Layer.provide(summary),
Layer.provide(Image.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true, outputTokenMax: 8_000 })),
Layer.provideMerge(deps),
),
),
LayerNode.buildLayer(LayerNode.group([root, LayerNode.make(TestLLMServer.layer, [])]), {
replacements: [
LayerNode.replace(SessionSummary.node, summary),
LayerNode.replace(
RuntimeFlags.node,
RuntimeFlags.layer({ experimentalEventSystem: true, outputTokenMax: 8_000 }),
),
],
}),
)
// kilocode_change end
+11 -4
View File
@@ -63,6 +63,7 @@ import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
import { reply, TestLLMServer } from "../lib/llm-server"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { MemoryService } from "@kilocode/kilo-memory/effect/service" // kilocode_change
import { RepositoryCache } from "@opencode-ai/core/repository-cache" // kilocode_change
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -225,6 +226,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Git.defaultLayer),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(RepositoryCache.defaultLayer), // kilocode_change - RepoCloneTool dependency
Layer.provide(Format.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provide(Auth.defaultLayer), // kilocode_change
@@ -705,7 +707,7 @@ it.instance("loop surfaces content-filter finishes as session errors", () =>
const off = yield* events.listen((event) => {
if (event.type !== Session.Event.Error.type) return Effect.void
const data = event.data as typeof Session.Event.Error.data.Type
if (data.sessionID === chat.id && data.error) errors.push(data.error)
if (data.sessionID === chat.id && data.error?.name === "ContentFilterError") errors.push(data.error)
return Effect.void
})
@@ -1046,7 +1048,7 @@ it.instance("subtask child inherits parent session external_directory allow", ()
}),
)
noLLMServer.instance("prompt tools replace previous prompt tool rules", () =>
noLLMServer.instance("prompt tools replace matching rules and preserve existing restrictions", () =>
Effect.gen(function* () {
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
@@ -1068,8 +1070,13 @@ noLLMServer.instance("prompt tools replace previous prompt tool rules", () =>
})
const reloaded = yield* sessions.get(session.id)
expect(reloaded.permission).toEqual([{ permission: "read", pattern: "*", action: "allow" }])
expect(Permission.evaluate("bash", "anything", reloaded.permission ?? []).action).toBe("ask")
// kilocode_change start - Kilo preserves existing restrictions that the new prompt does not override
expect(reloaded.permission).toEqual([
{ permission: "bash", pattern: "*", action: "deny" },
{ permission: "read", pattern: "*", action: "allow" },
])
expect(Permission.evaluate("bash", "anything", reloaded.permission ?? []).action).toBe("deny")
// kilocode_change end
}),
)
+1 -1
View File
@@ -489,7 +489,7 @@ describe("tool.task", () => {
]),
)
// kilocode_change end
expect(seen?.tools).toBeUndefined({
expect(seen?.tools).toEqual({
question: false, // kilocode_change - subagents cannot prompt the user directly
interactive_terminal: false, // kilocode_change - subagents cannot take over the user's terminal
todowrite: false,
+1
View File
@@ -10,6 +10,7 @@
"customConditions": ["browser"],
"paths": {
"@/*": ["./src/*"],
"@tui/*": ["../tui/src/*"], // kilocode_change - extracted TUI imports used by Kilo-owned components
"@test/*": ["./test/*"]
}
}
File diff suppressed because it is too large Load Diff
+3754 -1340
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -63,7 +63,7 @@ import * as Model from "./util/model"
import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig, type TuiConfig } from "./config"
import type { TuiConfig } from "./config"
import { createTuiApiAdapters } from "./plugin/adapters"
import { createTuiApi } from "./plugin/api"
import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime"
@@ -290,7 +290,8 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
: undefined
}
>
<TuiConfigProvider config={input.config}>
{/* kilocode_change - retain reactive Kilo TUI config hot reload after package extraction */}
<KiloApp.KiloTuiConfig.Provider config={input.config}>
<PluginRuntimeProvider value={pluginRuntime}>
<SDKProvider
url={input.url}
@@ -331,7 +332,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
</ProjectProvider>
</SDKProvider>
</PluginRuntimeProvider>
</TuiConfigProvider>
</KiloApp.KiloTuiConfig.Provider>
</RouteProvider>
</ToastProvider>
</KVProvider>
@@ -362,7 +363,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPluginHost }) {
const startup = useTuiStartup()
const tuiConfig = useTuiConfig()
const tuiConfig = KiloApp.KiloTuiConfig.use() // kilocode_change
const route = useRoute()
const dimensions = useTerminalDimensions()
const renderer = useRenderer()
+1 -1
View File
@@ -2,7 +2,7 @@
import { RGBA } from "@opentui/core"
import { For, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
import { tui } from "../../../../kilocode/cli/logo"
import { tui } from "@/kilocode/cli/logo"
// Shadow markers (rendered chars in parens):
// _ = full shadow cell (space with bg=shadow)
+3
View File
@@ -4,6 +4,7 @@ import { createBindingLookup } from "@opentui/keymap/extras"
import { Schema } from "effect"
import { createContext, type JSX, useContext } from "solid-js"
import { TuiKeybind } from "./keybind"
import { KiloTitleIcon } from "@/kilocode/cli/cmd/tui/title-icon" // kilocode_change
export const AttentionSoundName = Schema.Literals([
"default",
@@ -58,11 +59,13 @@ export const Info = Schema.Struct({
plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
leader_timeout: Schema.optional(LeaderTimeout),
attention: Schema.optional(Attention),
title_icon: Schema.optional(KiloTitleIcon.Value), // kilocode_change
prompt: Schema.optional(Prompt),
scroll_speed: Schema.optional(ScrollSpeed).annotate({ description: "TUI scroll speed" }),
scroll_acceleration: Schema.optional(ScrollAcceleration),
diff_style: Schema.optional(DiffStyle),
mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }),
vim: Schema.optional(Schema.Boolean), // kilocode_change - retain Kilo prompt editing mode
})
export type Info = Schema.Schema.Type<typeof Info>
+1 -1
View File
@@ -80,7 +80,7 @@ export function useEvent() {
const payload = normalizeSyncEvent(event.payload)
if (!payload) return
if (event.directory === "global" || event.project === project.project()) {
handler(payload, { workspace: event.workspace })
handler(payload, { directory: event.directory, workspace: event.workspace })
}
})
}
+122 -9
View File
@@ -23,6 +23,7 @@ import type {
ConsoleState,
BackgroundProcessInfo, // kilocode_change
InteractiveTerminalSnapshot, // kilocode_change
IndexingStatus, // kilocode_change
} from "@kilocode/sdk/v2"
import { createStore, produce, reconcile } from "solid-js/store"
import { useProject } from "./project"
@@ -38,7 +39,6 @@ import { useKV } from "./kv"
import { handleSuggestionEvent } from "@/kilocode/suggestion/tui/sync" // kilocode_change
import { appendTerminalOutput } from "@/kilocode/interactive-terminal/output" // kilocode_change
import { useToast } from "../ui/toast" // kilocode_change
import type { IndexingStatus } from "@kilocode/kilo-indexing/status" // kilocode_change
const emptyConsoleState: ConsoleState = {
consoleManagedProviders: [],
@@ -123,6 +123,7 @@ export const {
all: [],
default: {},
connected: [],
failed: [],
},
console_state: emptyConsoleState,
provider_auth: {},
@@ -159,11 +160,6 @@ export const {
const toast = useToast() // kilocode_change
// kilocode_change start
function processScope(scope: string) {
const current = project.instance.path()
return scope === current.directory || scope === current.worktree || scope === project.data.project.worktree
}
function evict(sessionID: string) {
const children = store.session.filter((session) => session.parentID === sessionID).map((session) => session.id)
setStore(
@@ -402,7 +398,6 @@ export const {
// kilocode_change start
case "background_process.updated": {
if (!processScope(event.properties.scope)) break
const info = event.properties.info
deleted.delete(info.id)
setStore(
@@ -423,7 +418,6 @@ export const {
break
}
case "background_process.deleted": {
if (!processScope(event.properties.scope)) break
deleted.add(event.properties.processID)
setStore(
"background_process",
@@ -439,7 +433,6 @@ export const {
break
}
case "interactive_terminal.updated": {
if (!processScope(event.properties.scope)) break
const info = event.properties.info
terminalDeleted.delete(info.id)
const list = store.interactive_terminal[info.sessionID] ?? []
@@ -626,6 +619,126 @@ export const {
}
})
// kilocode_change start - retain versioned Sync events used by Kilo clients
event.sync((event) => {
switch (event.name) {
case "session.created.1": {
const info = event.data.info
const match = search(store.session, info.id, (item) => item.id)
if (match.found) setStore("session", match.index, reconcile(info))
if (!match.found)
setStore(
"session",
produce((draft) => draft.splice(match.index, 0, info)),
)
break
}
case "session.updated.1": {
const id = event.data.sessionID
const match = search(store.session, id, (item) => item.id)
if (!match.found) break
setStore(
"session",
match.index,
produce((draft) => void Object.assign(draft, event.data.info)),
)
break
}
case "session.deleted.1": {
const id = event.data.sessionID
const match = search(store.session, id, (item) => item.id)
if (match.found)
setStore(
"session",
produce((draft) => draft.splice(match.index, 1)),
)
evict(id)
break
}
case "message.updated.1": {
touchMessage(event.data.info.sessionID, event.data.info.id)
const info = strip(event.data.info)
const messages = store.message[info.sessionID]
if (!messages) {
setStore("message", info.sessionID, [info])
break
}
const match = search(messages, info.id, (item) => item.id)
if (match.found) {
setStore("message", info.sessionID, match.index, reconcile(info))
break
}
setStore(
"message",
info.sessionID,
produce((draft) => draft.splice(match.index, 0, info)),
)
const updated = store.message[info.sessionID]
if (updated.length <= 100) break
const oldest = updated[0]
batch(() => {
setStore(
"message",
info.sessionID,
produce((draft) => draft.shift()),
)
setStore(
"part",
produce((draft) => void delete draft[oldest.id]),
)
})
break
}
case "message.removed.1": {
touchMessage(event.data.sessionID, event.data.messageID)
const messages = store.message[event.data.sessionID]
if (!messages) break
const match = search(messages, event.data.messageID, (item) => item.id)
if (!match.found) break
setStore(
"message",
event.data.sessionID,
produce((draft) => draft.splice(match.index, 1)),
)
break
}
case "message.part.updated.1": {
touchPart(event.data.sessionID, event.data.part.id)
const part = event.data.part
const parts = store.part[part.messageID]
if (!parts) {
setStore("part", part.messageID, [part])
break
}
const match = search(parts, part.id, (item) => item.id)
if (match.found) {
setStore("part", part.messageID, match.index, reconcile(part))
break
}
setStore(
"part",
part.messageID,
produce((draft) => draft.splice(match.index, 0, part)),
)
break
}
case "message.part.removed.1": {
touchPart(event.data.sessionID, event.data.partID)
const parts = store.part[event.data.messageID]
if (!parts) break
const match = search(parts, event.data.partID, (item) => item.id)
if (!match.found) break
setStore(
"part",
event.data.messageID,
produce((draft) => draft.splice(match.index, 1)),
)
break
}
}
})
// kilocode_change end
const exit = useExit()
const args = useArgs()
+2 -1
View File
@@ -2700,6 +2700,7 @@ function Question(props: ToolProps) {
const { theme } = useTheme()
const questions = createMemo(() => parseQuestions(props.input.questions))
const answers = createMemo(() => parseQuestionAnswers(props.metadata.answers))
const dismissed = createMemo(() => props.metadata.dismissed === true) // kilocode_change
const count = createMemo(() => questions().length)
function format(answer?: ReadonlyArray<string>) {
@@ -2711,7 +2712,7 @@ function Question(props: ToolProps) {
const title = createMemo(() => (dismissed() ? "# Questions (dismissed)" : "# Questions"))
const subtitle = createMemo(() => {
if (dismissed()) return `${count()} dismissed`
if ((props.metadata.answers?.length ?? 0) > 0) return `${count()} answered`
if ((answers()?.length ?? 0) > 0) return `${count()} answered`
return `${count()} question${count() !== 1 ? "s" : ""}`
})
// kilocode_change end
+2 -2
View File
@@ -2,11 +2,11 @@
/** @jsxImportSource @opentui/solid */
import { Show, createEffect, createSignal, onCleanup } from "solid-js"
import { useTheme } from "../../context/theme"
import { SplitBorder } from "../../component/border"
import { SplitBorder } from "../../ui/border"
import { useSDK } from "../../context/sdk"
import { useDialog } from "../../ui/dialog"
import type { SessionNetworkWait } from "@kilocode/sdk/v2"
import { useTuiConfig } from "../../context/tui-config"
import { useTuiConfig } from "../../config"
import { useBindings } from "../../keymap"
export function NetworkPrompt(props: { request: SessionNetworkWait }) {
+1 -1
View File
@@ -1,2 +1,2 @@
// kilocode_change - new file
export { SuggestPrompt } from "../../../../../kilocode/suggestion/tui/prompt"
export { SuggestPrompt } from "@/kilocode/suggestion/tui/prompt"
+1 -1
View File
@@ -3,7 +3,7 @@ import { TextAttributes, decodePasteBytes, type MouseEvent, type PasteEvent } fr
import { useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/solid"
import type { InteractiveTerminalSnapshot } from "@kilocode/sdk/v2"
import { VtScreen } from "@/kilocode/cli/cmd/tui/vt/vt-screen"
import { SplitBorder } from "@tui/component/border"
import { SplitBorder } from "@tui/ui/border"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
import { useTheme } from "@tui/context/theme"
+2 -2
View File
@@ -45,7 +45,7 @@ export interface DialogSelectProps<T> {
}
// kilocode_change start - support list-level actions when no option is selected
type DialogSelectActionBase = {
type DialogSelectActionBase<T> = {
command: string
title: string
side?: "left" | "right"
@@ -53,7 +53,7 @@ type DialogSelectActionBase = {
disabled?: boolean | ((option: DialogSelectOption<T> | undefined) => boolean)
}
type DialogSelectAction<T> = DialogSelectActionBase &
type DialogSelectAction<T> = DialogSelectActionBase<T> &
(
| {
requiresSelection?: true
@@ -6,7 +6,8 @@ import { KVProvider, useKV } from "../../../../src/context/kv"
import { ProjectProvider, useProject } from "../../../../src/context/project"
import { SDKProvider } from "../../../../src/context/sdk"
import { SyncProvider, useSync } from "../../../../src/context/sync"
import { ToastProvider } from "../../../../src/cli/cmd/tui/ui/toast" // kilocode_change
import { ToastProvider } from "../../../../src/ui/toast" // kilocode_change
import { ExitProvider } from "../../../../src/context/exit" // kilocode_change
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
import { TestTuiContexts } from "../../../fixture/tui-environment"
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
@@ -52,9 +53,13 @@ export async function mount(override?: FetchHandler, state?: string) {
{/* kilocode_change end */}
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
<ProjectProvider>
<SyncProvider>
<Probe />
</SyncProvider>
{/* kilocode_change start - SyncProvider consumes the exit context */}
<ExitProvider exit={() => {}}>
<SyncProvider>
<Probe />
</SyncProvider>
</ExitProvider>
{/* kilocode_change end */}
</ProjectProvider>
</SDKProvider>
{/* kilocode_change start */}
@@ -7,7 +7,7 @@
* `messages.data!` while the SDK leaves `data` undefined on error.
*/
import { describe, expect, test } from "bun:test"
import { disposeAllInstances, provideTestInstance, tmpdir } from "../../../fixture/fixture"
import { tmpdir } from "../../../fixture/fixture"
import { directory, json, mount } from "./sync-fixture"
const sessionID = "ses_undef"
@@ -25,26 +25,19 @@ describe("tui sync (#26560)", () => {
directory,
project_id: "proj_test",
}
// kilocode_change start
const { app, sync } = await provideTestInstance({
directory: tmp.path,
fn: () =>
mount((url) => {
if (url.pathname === `/session/${sessionID}`) return json(sessionPayload)
if (url.pathname === `/session/${sessionID}/messages`) return json({}, { status: 500 })
if (url.pathname === `/session/${sessionID}/todo`) return json([])
if (url.pathname === `/session/${sessionID}/diff`) return json([])
if (url.pathname === "/session") return json([sessionPayload])
return undefined
}, tmp.path),
})
// kilocode_change end
const { app, sync } = await mount((url) => {
if (url.pathname === `/session/${sessionID}`) return json(sessionPayload)
if (url.pathname === `/session/${sessionID}/messages`) return json({}, { status: 500 })
if (url.pathname === `/session/${sessionID}/todo`) return json([])
if (url.pathname === `/session/${sessionID}/diff`) return json([])
if (url.pathname === "/session") return json([sessionPayload])
return undefined
}, tmp.path)
try {
await expect(sync.session.sync(sessionID)).resolves.toBeUndefined()
} finally {
app.renderer.destroy()
await disposeAllInstances() // kilocode_change
}
})
})
+7 -5
View File
@@ -1,7 +1,7 @@
/** @jsxImportSource @opentui/solid */
import { describe, expect, test } from "bun:test"
import { disposeAllInstances, provideTestInstance, tmpdir } from "../../../fixture/fixture"
import { mount, wait } from "./sync-fixture"
import { tmpdir } from "../../../fixture/fixture"
import { json, mount, wait } from "./sync-fixture"
import type { GlobalEvent } from "@kilocode/sdk/v2"
function branchEvent(branch: string, workspace?: string): GlobalEvent {
@@ -21,7 +21,7 @@ describe("tui sync", () => {
test("refresh scopes sessions by default and lists project sessions when disabled", async () => {
await using tmp = await tmpdir()
await Bun.write(`${tmp.path}/kv.json`, "{}")
const { app, kv, sync, session } = await provideTestInstance({ directory: tmp.path, fn: () => mount(undefined, tmp.path) }) // kilocode_change
const { app, kv, sync, session } = await mount(undefined, tmp.path)
try {
expect(kv.get("session_directory_filter_enabled", true)).toBe(true)
@@ -35,14 +35,16 @@ describe("tui sync", () => {
expect(session.at(-1)?.searchParams.get("path")).toBeNull()
} finally {
app.renderer.destroy()
await disposeAllInstances() // kilocode_change
}
})
test("vcs branch updates only apply for the active workspace", async () => {
await using tmp = await tmpdir()
await Bun.write(`${tmp.path}/kv.json`, "{}")
const { app, emit, project, sync } = await mount(undefined, tmp.path)
const { app, emit, project, sync } = await mount(
(url) => (url.pathname === "/experimental/workspace" ? json([{ id: "ws_a" }]) : undefined),
tmp.path,
) // kilocode_change - workspace re-bootstrap retains the selected test workspace
try {
expect(sync.data.vcs?.branch).toBe("main")
+11 -1
View File
@@ -54,8 +54,18 @@ export function createFetch(override?: FetchHandler) {
].includes(url.pathname)
)
return json([])
if (["/config", "/experimental/resource", "/mcp", "/provider/auth", "/session/status"].includes(url.pathname))
if (
["/config", "/global/config", "/experimental/resource", "/mcp", "/provider/auth", "/session/status"].includes(
url.pathname,
)
)
return json({})
// kilocode_change start - Kilo bootstrap endpoints
if (["/network", "/background-process", "/interactive-terminal", "/config/warnings"].includes(url.pathname))
return json([])
if (url.pathname === "/indexing/status")
return json({ state: "Disabled", message: "Indexing disabled.", processedFiles: 0, totalFiles: 0, percent: 0 })
// kilocode_change end
if (url.pathname === "/config/providers") return json({ providers: {}, default: {} })
if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
+9 -2
View File
@@ -5,6 +5,13 @@
"jsx": "preserve",
"jsxImportSource": "@opentui/solid",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
"noUncheckedIndexedAccess": false,
// kilocode_change - resolve Kilo integrations retained during upstream's standalone TUI extraction
"paths": {
"@/*": ["../opencode/src/*"],
"@tui/*": ["./src/*"]
}
},
// kilocode_change - include ambient declarations used by retained Kilo imports from opencode
"include": ["src", "test", "../opencode/src/*.d.ts"]
}
+2 -1
View File
@@ -24,11 +24,12 @@ const allow: Record<string, string> = {
"cli/cmd/run/runtime.boot.ts": "direct run startup resolver runtime boundary",
"cli/cmd/run/stream.transport.ts": "per-subscription direct run transport runtime boundary",
"cli/cmd/run/variant.shared.ts": "direct run variant persistence runtime boundary with test filesystem injection",
"cli/cmd/tui/config/tui.ts": "separately tracked TUI config facade",
"config/tui.ts": "separately tracked TUI config facade moved by the upstream TUI extraction",
"installation/index.ts": "existing installation facade outside #10655",
}
const testAllow: Record<string, { count: number; reason: string }> = {
"preload.ts": { count: 2, reason: "global test-suite AppRuntime cleanup boundary" },
"kilocode/config-resilience.test.ts": { count: 4, reason: "existing runtime integration test" },
"kilocode/config-validation.test.ts": { count: 2, reason: "existing runtime integration test" },
"kilocode/cli-shutdown.test.ts": { count: 1, reason: "mocked runtime boundary for shutdown unit tests" },