fix(cli): reduce startup initialization work

This commit is contained in:
marius-kilocode
2026-08-28 15:49:59 +02:00
parent f93583fb43
commit 8a8f67f9c7
12 changed files with 172 additions and 35 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Start the CLI faster while preserving available models, session resumption, and configured reference permissions.
+7 -12
View File
@@ -26,7 +26,7 @@ import { Effect, Context, Layer, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
import type { DeepMutable } from "@opencode-ai/core/schema" // kilocode_change
// kilocode_change start
import * as KiloAgent from "@/kilocode/agent"
import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -35,9 +35,7 @@ import * as KiloReference from "@/kilocode/reference"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { Reference } from "@opencode-ai/core/reference"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
// kilocode_change
export const Info = Schema.Struct({
name: Schema.String,
@@ -113,17 +111,14 @@ const layer = Layer.effect(
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* () {
if (Object.keys(cfg.references ?? cfg.reference ?? {}).length) {
yield* (yield* PluginV2.Service).wait(PluginV2.ID.make("core/config-reference"))
}
yield* KiloReference.sync({
const referenceDirs = yield* KiloReference.list(
{
references: cfg.references ?? cfg.reference ?? {},
directory: ctx.directory,
worktree: ctx.worktree,
})
return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
},
locations,
).pipe(Effect.map((references) => references.map((reference) => reference.path)))
const whitelistedDirs = [
Truncate.GLOB,
path.join(Global.Path.tmp, "*"),
+3 -2
View File
@@ -14,7 +14,7 @@ import type { EventSource } from "@opencode-ai/tui/context/sdk"
import { writeHeapSnapshot } from "v8"
import type { StartInput } from "@/kilocode/cli/cmd/tui/thread" // kilocode_change - runtime imports deferred into handlers
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
import { validateSession } from "../tui/validate-session"
import { validate as validateSession } from "@/kilocode/cli/cmd/tui" // kilocode_change
// kilocode_change start - correlate the TUI worker with its parent process
import {
KILO_PROCESS_ROLE,
@@ -276,7 +276,7 @@ export const TuiThreadCommand = cmd({
exiting: false,
}
try {
const { TuiConfig } = await import("@/config/tui")
// kilocode_change
if (args.fork && !args.continue && !args.session) {
UI.error("--fork requires --continue or --session")
process.exitCode = 1
@@ -418,6 +418,7 @@ export const TuiThreadCommand = cmd({
// kilocode_change end
const prompt = await input(args.prompt)
const { TuiConfig } = await import("@/config/tui") // kilocode_change
const config = await TuiConfig.get()
const network = resolveNetworkOptionsNoConfig(args)
@@ -2,3 +2,9 @@ export function preload(compiled: boolean, resolve: () => string) {
if (compiled) return []
return [resolve()]
}
export async function validate(input: Parameters<typeof import("@/cli/tui/validate-session").validateSession>[0]) {
if (!input.sessionID) return
const { validateSession } = await import("@/cli/tui/validate-session")
return validateSession(input)
}
@@ -4,11 +4,8 @@ import type { NetworkOptions } from "@/cli/network"
import { ServerAuth } from "@/server/auth"
import { Flag } from "@opencode-ai/core/flag/flag"
import { errorMessage } from "@opencode-ai/tui/util/error"
import { TuiConfig } from "@/config/tui"
import { validateSession } from "@/cli/tui/validate-session"
import { importCloudSession, reportCloudImportError } from "@/kilocode/cloud-session"
import { validate as validateSession } from "@/kilocode/cli/cmd/tui"
import { DaemonClient } from "@/kilocode/daemon/client"
import { createKiloClient } from "@kilocode/sdk/v2"
type TuiInput = import("@opencode-ai/tui").TuiInput
export type StartInput = Omit<TuiInput, "pluginHost">
@@ -33,6 +30,10 @@ type Input = {
async function session(input: Input, daemon: DaemonClient.Connection) {
if (!input.args.cloudFork || !input.args.session) return { ok: true as const, id: input.args.session }
const [{ createKiloClient }, { importCloudSession, reportCloudImportError }] = await Promise.all([
import("@kilocode/sdk/v2"),
import("@/kilocode/cloud-session"),
])
UI.println("Importing session from cloud...")
const client = createKiloClient({
baseUrl: daemon.url,
@@ -67,6 +68,7 @@ export namespace KiloTuiThreadDaemon {
if (!daemon) return false
const prompt = await input.input()
const { TuiConfig } = await import("@/config/tui")
const config = await TuiConfig.get()
const fork = await session(input, daemon)
+18 -1
View File
@@ -2,7 +2,10 @@ 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"
import { Effect } from "effect"
import { type Context, Effect, RcMap } from "effect"
import { Location } from "@opencode-ai/core/location"
import type { LocationServiceMap } from "@opencode-ai/core/location-services"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Reference } from "@opencode-ai/core/reference"
import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -189,3 +192,17 @@ export const sync = Effect.fn("KiloReference.sync")(function* (input: {
if (current.size === sources.length && sources.every(([name, source]) => same(current.get(name), source))) return
yield* service.replace(sources)
})
export const list = Effect.fn("KiloReference.list")(function* (
input: { references: ConfigReference.Info; directory: string; worktree: string },
locations: Context.Service.Shape<typeof LocationServiceMap.Service>,
) {
const location = Location.Ref.make({ directory: AbsolutePath.make(input.directory) })
const configured = Object.keys(input.references).length > 0
if (!configured && !(yield* RcMap.has(locations.rcMap, location))) return []
return yield* Effect.gen(function* () {
if (configured) yield* (yield* PluginV2.Service).wait(PluginV2.ID.make("core/config-reference"))
yield* sync(input)
return yield* (yield* Reference.Service).list()
}).pipe(Effect.provide(locations.get(location)))
})
+6 -12
View File
@@ -19,13 +19,10 @@ import type { Provider } from "@/provider/provider"
import type { Agent } from "@/agent/agent"
import { Permission } from "@/permission"
import { Skill } from "@/skill"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Location } from "@opencode-ai/core/location"
// kilocode_change
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { Reference } from "@opencode-ai/core/reference"
import { MCP } from "@/mcp"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { PluginV2 } from "@opencode-ai/core/plugin" // kilocode_change
// kilocode_change start
import SOUL from "../kilocode/soul.txt"
@@ -114,17 +111,14 @@ const layer = Layer.effect(
) {
const ctx = yield* InstanceState.context
const cfg = yield* config.get()
const references = yield* Effect.gen(function* () {
if (Object.keys(cfg.references ?? cfg.reference ?? {}).length) {
yield* (yield* PluginV2.Service).wait(PluginV2.ID.make("core/config-reference"))
}
yield* KiloReference.sync({
const references = yield* KiloReference.list(
{
references: cfg.references ?? cfg.reference ?? {},
directory: ctx.directory,
worktree: ctx.worktree,
})
return (yield* (yield* Reference.Service).list()).filter((reference) => reference.description !== undefined)
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
},
locations,
).pipe(Effect.map((references) => references.filter((reference) => reference.description !== undefined)))
return [
...KilocodeSystemPrompt.environment({ ctx, model, editor: editorContext }),
references.length === 0
@@ -10,7 +10,7 @@ import {
resolveThreadDirectory,
runEmbeddedRemoteExitBridge,
} from "../../../../src/cli/cmd/tui"
import { preload } from "../../../../src/kilocode/cli/cmd/tui"
import { preload, validate } from "../../../../src/kilocode/cli/cmd/tui"
import { KiloTuiThreadDaemon } from "../../../../src/kilocode/cli/cmd/tui/thread"
import { DaemonClient } from "../../../../src/kilocode/daemon/client"
@@ -19,6 +19,14 @@ afterEach(() => {
})
describe("kilo tui thread", () => {
test("starts fresh sessions without requesting session validation", async () => {
await expect(validate({ url: "http://127.0.0.1:0" })).resolves.toBeUndefined()
})
test("still rejects invalid IDs when resuming a session", async () => {
await expect(validate({ url: "http://127.0.0.1:0", sessionID: "invalid" })).rejects.toThrow("Invalid session ID")
})
test("skips preload resolver invocation in compiled mode", () => {
let calls = 0
@@ -1,7 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { describe, expect, test } from "bun:test"
import path from "path"
import { Cause, Effect, Exit, Layer } from "effect"
import { Cause, Effect, Exit, Layer, RcMap } from "effect"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import * as Reference from "../../src/kilocode/reference"
import { Reference as CoreReference } from "@opencode-ai/core/reference"
@@ -25,6 +25,37 @@ function remote() {
}
describe("configured references", () => {
test("does not initialize location services for an empty reference configuration", async () => {
await using tmp = await tmpdir()
const result = await Effect.runPromise(
Effect.gen(function* () {
const map = yield* LocationServiceMap.Service
const references = yield* Reference.list({ references: {}, directory: tmp.path, worktree: tmp.path }, map)
return { references, keys: Array.from(yield* RcMap.keys(map.rcMap)) }
}).pipe(Effect.provide(buildLocationServiceMap()), Effect.scoped),
)
expect(result).toEqual({ references: [], keys: [] })
})
test("clears previously initialized references when configuration becomes empty", async () => {
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
const result = await Effect.runPromise(
Effect.gen(function* () {
const map = yield* LocationServiceMap.Service
const input = { directory: tmp.path, worktree: tmp.path }
const before = yield* Reference.list({ ...input, references: { docs: "./docs" } }, map)
const after = yield* Reference.list({ ...input, references: {} }, map)
const persisted = yield* CoreReference.Service.use((service) => service.list()).pipe(
Effect.provide(map.get(Location.Ref.make({ directory: AbsolutePath.make(tmp.path) }))),
)
return { before, after, persisted }
}).pipe(Effect.provide(buildLocationServiceMap()), Effect.scoped),
)
expect(result.before.map((reference) => reference.path)).toEqual([AbsolutePath.make(path.join(tmp.path, "docs"))])
expect(result.after).toEqual([])
expect(result.persisted).toEqual([])
}, 15_000)
test("preserves interruption while materializing a repository", async () => {
const cache = RepositoryCache.Service.of({ ensure: () => Effect.interrupt })
const exit = await Effect.runPromiseExit(Reference.ensure(cache, remote()))
@@ -0,0 +1,37 @@
import { expect, test } from "bun:test"
import { fileURLToPath } from "node:url"
test("TUI configuration does not load the terminal renderer", async () => {
const entry = fileURLToPath(new URL("../../../tui/src/config/index.tsx", import.meta.url))
const config = fileURLToPath(new URL("../../tsconfig.json", import.meta.url))
const code = `
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
const result = await Bun.build({
entrypoints: [${JSON.stringify(entry)}],
target: "bun",
tsconfig: ${JSON.stringify(config)},
conditions: ["bun", "node"],
write: false,
plugins: [
createSolidTransformPlugin(),
{
name: "config-renderer-boundary",
setup(build) {
build.onResolve({ filter: /^@opentui\\/(?:core|solid)(?:\\/|$)/ }, (args) => {
throw new Error("Configuration loaded the terminal renderer: " + args.path)
})
},
},
],
})
if (!result.success) throw new AggregateError(result.logs)
`
const proc = Bun.spawn([process.execPath, "--eval", code], {
cwd: fileURLToPath(new URL("../..", import.meta.url)),
stdout: "pipe",
stderr: "pipe",
windowsHide: true,
})
const [exit, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()])
expect({ exit, stderr }).toEqual({ exit: 0, stderr: "" })
})
+11 -2
View File
@@ -2,7 +2,7 @@ export * as TuiConfig from "."
import { createBindingLookup } from "@opentui/keymap/extras"
import { Schema } from "effect"
import { createContext, type JSX, useContext } from "solid-js"
import { createComponent, createContext, type JSX, useContext } from "solid-js" // kilocode_change
import { TuiKeybind } from "./keybind"
import { KiloTitleIcon } from "@/kilocode/cli/cmd/tui/title-icon" // kilocode_change
@@ -122,7 +122,16 @@ export function resolve(input: Info, options: ResolveOptions): Resolved {
const ConfigContext = createContext<Resolved>()
export function TuiConfigProvider(props: { config: Resolved; children: JSX.Element }) {
return <ConfigContext.Provider value={props.config}>{props.children}</ConfigContext.Provider>
// kilocode_change start
return createComponent(ConfigContext.Provider, {
get value() {
return props.config
},
get children() {
return props.children
},
})
// kilocode_change end
}
export function useTuiConfig() {
@@ -0,0 +1,32 @@
import { testRender } from "@opentui/solid"
import { expect, test } from "bun:test"
import { createSignal } from "solid-js"
import { resolve, TuiConfigProvider, useTuiConfig } from "../../src/config"
test("preserves reactive configuration reads through the provider", async () => {
const [theme, update] = createSignal("first")
const config = {
...resolve({}, { terminalSuspend: true }),
get theme() {
return theme()
},
}
function Consumer() {
const value = useTuiConfig()
return <text>{value.theme}</text>
}
const app = await testRender(() => (
<TuiConfigProvider config={config}>
<Consumer />
</TuiConfigProvider>
))
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("first")
update("next")
await app.renderOnce()
expect(app.captureCharFrame()).toContain("next")
} finally {
app.renderer.destroy()
}
})