mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
fix(cli): reduce startup initialization work
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Start the CLI faster while preserving available models, session resumption, and configured reference permissions.
|
||||
@@ -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, "*"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)))
|
||||
})
|
||||
|
||||
@@ -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: "" })
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user