mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge branch 'main' into feat/mcp-apps-experimental
This commit is contained in:
@@ -1,5 +1,61 @@
|
||||
# @kilocode/cli
|
||||
|
||||
## 7.4.23
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#13137](https://github.com/Kilo-Org/kilocode/pull/13137) [`90a93a7`](https://github.com/Kilo-Org/kilocode/commit/90a93a7aa25950d5894fa67f6e4e6545ef55017c) - Add `kilo pr link <url>`, `kilo pr unlink`, and `kilo pr status` to link the current worktree to a pull request. The checkout command moves to `kilo pr checkout <number>`; `kilo pr <number>` no longer checks out a PR.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#13206](https://github.com/Kilo-Org/kilocode/pull/13206) [`e13c6d3`](https://github.com/Kilo-Org/kilocode/commit/e13c6d3ee93d123c6fc187592fc28324c8840fc2) - Fix Agent Manager ignoring requests to start new sessions on OpenAI Responses API models, where a start request was answered with a list of existing sessions instead of creating the worktree or session.
|
||||
|
||||
- [#13124](https://github.com/Kilo-Org/kilocode/pull/13124) [`d4f3a3a`](https://github.com/Kilo-Org/kilocode/commit/d4f3a3a9e63a3954214887563dc3816ea179858f) - Stop broad permission rules from letting Ask and Plan modes change your workspace. Catch-all approvals, the "Allow everything" toggle, and the `<command> *` rules that "Always allow" persists no longer grant these modes shell commands, subagents, notebook edits or other mutating tools, and MCP tools go back to prompting. To opt a single mode in, set `agent.ask.permission` or `agent.plan.permission` instead of a top-level `permission` rule.
|
||||
|
||||
- [#13121](https://github.com/Kilo-Org/kilocode/pull/13121) [`e2966ab`](https://github.com/Kilo-Org/kilocode/commit/e2966abcba4906383d13b385d61ecd50016e4d7d) - Keep the selected agent after switching from Ask to Code, and remind the model that previous Ask-mode restrictions no longer apply.
|
||||
|
||||
- [#13224](https://github.com/Kilo-Org/kilocode/pull/13224) [`b1755f9`](https://github.com/Kilo-Org/kilocode/commit/b1755f91848b083533e675ba38750063862344d4) - Keep one-time waits in the blocking shell tool instead of tracking them as background processes.
|
||||
|
||||
- [#13209](https://github.com/Kilo-Org/kilocode/pull/13209) [`ff16bc2`](https://github.com/Kilo-Org/kilocode/commit/ff16bc2c9c25d4f11af5303cd79267546456cb1e) - Keep Agent Manager terminals and nested Kilo sessions alive across configuration reloads and location idle eviction, while cleaning them up on explicit close, worktree deletion, and server shutdown.
|
||||
|
||||
- [#13115](https://github.com/Kilo-Org/kilocode/pull/13115) [`d9f0eff`](https://github.com/Kilo-Org/kilocode/commit/d9f0eff30634410738c82355641b4e6353c135bb) - Start Kilo with a persistent fallback when the default runtime state directory is not writable.
|
||||
|
||||
- [#13210](https://github.com/Kilo-Org/kilocode/pull/13210) [`8d717a0`](https://github.com/Kilo-Org/kilocode/commit/8d717a05d322a2382af05d8265ec2354c893a674) - Remove the duplicate skill catalog from the model-facing skill tool description.
|
||||
|
||||
- [#13183](https://github.com/Kilo-Org/kilocode/pull/13183) [`017410b`](https://github.com/Kilo-Org/kilocode/commit/017410bf6fbfdaa1e3a050f00f7e0cd3dd5371ad) - Accept JWT share tokens when importing a session from a Kilo share URL.
|
||||
|
||||
- [#13165](https://github.com/Kilo-Org/kilocode/pull/13165) [`5e6e93a`](https://github.com/Kilo-Org/kilocode/commit/5e6e93aa19e984d8699d1a3199b49f8e726e899c) - Use the full Codex context window for GPT-5.6 models authenticated through ChatGPT OAuth.
|
||||
|
||||
- [#13249](https://github.com/Kilo-Org/kilocode/pull/13249) [`3de7df2`](https://github.com/Kilo-Org/kilocode/commit/3de7df279bceab97fc1fd75eb0a9e45e735c472d) - Require the todo tool to update multi-step lists after each completed task.
|
||||
|
||||
- [#13122](https://github.com/Kilo-Org/kilocode/pull/13122) [`c34a2a3`](https://github.com/Kilo-Org/kilocode/commit/c34a2a3a42f9f07d6ae2643ec655720bdb84c820) - Stop memory auto-save from failing on OpenAI-compatible providers that stream by default.
|
||||
|
||||
- [#13195](https://github.com/Kilo-Org/kilocode/pull/13195) [`f54c215`](https://github.com/Kilo-Org/kilocode/commit/f54c215e6ab2f22e055790ccc4a2d122992dba48) Thanks [@quanzhuo](https://github.com/quanzhuo)! - Persist disabling snapshots from the slow-repo prompt across restarts.
|
||||
|
||||
- [#13112](https://github.com/Kilo-Org/kilocode/pull/13112) [`2eb6300`](https://github.com/Kilo-Org/kilocode/commit/2eb630053f5bb822eb6a2b9e830afecfd0f6163c) - Switch to the code model when starting implementation after a planning session.
|
||||
|
||||
- [#13225](https://github.com/Kilo-Org/kilocode/pull/13225) [`da10638`](https://github.com/Kilo-Org/kilocode/commit/da1063865480bb7cd2aeeb8c18a949a805bd4872) - Remove the experimental agent requirements check and its configuration flag.
|
||||
|
||||
- [#13214](https://github.com/Kilo-Org/kilocode/pull/13214) [`e1bcb32`](https://github.com/Kilo-Org/kilocode/commit/e1bcb320d94efe6ed8abf7c33fe6475a67a2e1e7) - Remove the experimental task-aware tool-output pruning feature and its related settings and indicators.
|
||||
|
||||
- [#13199](https://github.com/Kilo-Org/kilocode/pull/13199) [`6131ed2`](https://github.com/Kilo-Org/kilocode/commit/6131ed269f37ae8e258c1b91929c3170a4cf2767) - Keep `kilo upgrade` on the Kilo CLI release channel when GitHub's latest release is a JetBrains release.
|
||||
|
||||
- [#13178](https://github.com/Kilo-Org/kilocode/pull/13178) [`86af8dd`](https://github.com/Kilo-Org/kilocode/commit/86af8dd7c700fcb6229f28471126b5e7b0f6f654) - Prompt for explicit, one-shot approval before mutating Git commands run outside the sandbox.
|
||||
|
||||
- [#13103](https://github.com/Kilo-Org/kilocode/pull/13103) [`591772d`](https://github.com/Kilo-Org/kilocode/commit/591772d92875762460636b42233ad1ad552e8596) - Use provider model catalogs instead of hardcoded model-name heuristics for reasoning variants.
|
||||
|
||||
- [#13170](https://github.com/Kilo-Org/kilocode/pull/13170) [`3acb1ec`](https://github.com/Kilo-Org/kilocode/commit/3acb1ec38693e7f75bb38e832a23ace097c7440a) - Keep recently used Kilo Gateway models visible in the TUI picker, and find them when filtering by kilo.
|
||||
|
||||
- [#13247](https://github.com/Kilo-Org/kilocode/pull/13247) [`0d5d334`](https://github.com/Kilo-Org/kilocode/commit/0d5d334480bc2093a12b27a34b03cac88cf33422) - Fix TUI sessions where new turns stopped appearing until the session was reopened
|
||||
|
||||
- Updated dependencies [[`f39e163`](https://github.com/Kilo-Org/kilocode/commit/f39e1631855859222966350bd5fae373b9877297)]:
|
||||
- @kilocode/kilo-gateway@8.0.0
|
||||
- @opencode-ai/server@7.4.23
|
||||
- @opencode-ai/tui@7.4.23
|
||||
- @opencode-ai/ui@7.4.23
|
||||
- @kilocode/kilo-indexing@7.4.23
|
||||
- @kilocode/kilo-telemetry@7.4.23
|
||||
|
||||
## 7.4.22
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.4.22",
|
||||
"version": "7.4.23",
|
||||
"name": "@kilocode/cli",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { AlibabaProviderOptions } from "@ai-sdk/alibaba"
|
||||
import type { AnthropicProviderOptions } from "@ai-sdk/anthropic"
|
||||
import type { MistralLanguageModelOptions } from "@ai-sdk/mistral"
|
||||
import type { OpenAIResponsesProviderOptions } from "@ai-sdk/openai"
|
||||
import type { OpenAICompatibleProviderOptions } from "@ai-sdk/openai-compatible"
|
||||
import type { OpenRouterProviderOptions } from "@openrouter/ai-sdk-provider"
|
||||
@@ -27,11 +25,5 @@ export function kiloProviderOptions(options: { [x: string]: any }) {
|
||||
openrouter.reasoning && "effort" in openrouter.reasoning ? openrouter.reasoning?.effort : undefined,
|
||||
textVerbosity: openrouter.verbosity,
|
||||
} satisfies OpenAICompatibleProviderOptions
|
||||
result.alibaba = {
|
||||
enableThinking: openrouter.reasoning?.enabled,
|
||||
} satisfies AlibabaProviderOptions
|
||||
result.mistral = {
|
||||
reasoningEffort: openrouter.reasoning?.enabled ? "high" : undefined,
|
||||
} satisfies MistralLanguageModelOptions
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -205,9 +205,7 @@ export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader>
|
||||
options,
|
||||
async getModel(sdk: KiloProvider, modelID: string) {
|
||||
const provider = input.models[modelID]?.ai_sdk_provider
|
||||
if (provider === "alibaba") return sdk.alibaba(modelID)
|
||||
if (provider === "anthropic") return sdk.anthropic(modelID)
|
||||
if (provider === "mistral") return sdk.mistral(modelID)
|
||||
if (provider === "openai") return sdk.openai(modelID)
|
||||
if (provider === "openai-compatible") return sdk.openaiCompatible(modelID)
|
||||
return sdk.languageModel(modelID)
|
||||
|
||||
@@ -451,6 +451,7 @@ export namespace KiloSessionPrompt {
|
||||
info,
|
||||
"Use the chosen plan path as the main plan file. Do not write or edit other files unless the user explicitly asks and your permissions allow it.",
|
||||
"Project/user instructions about plan location (for example plans/ or .plans/) are authorized when permissions allow them; they do not conflict with this reminder. When finalizing, call plan_exit with the path of the plan file you wrote.",
|
||||
"In the visible final response, cite the saved plan path as an inline code span so the client can open it as a document. Cite other user-facing files you create the same way instead of pasting the full file into chat.",
|
||||
supportsPlanFollowup()
|
||||
? "When the plan is implementation-ready, write the main plan file and call plan_exit. Do not ask the user to choose between finalizing and refining in chat; the client follow-up after plan_exit asks whether to implement the saved plan or keep refining."
|
||||
: 'Before creating or updating the plan file, or calling plan_exit, ask the user to choose exactly one of: "Finalize and save the plan" or "Continue refining". If the user chooses to finalize, write the main plan file, then call plan_exit.',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { registerDisposer } from "@/effect/instance-registry"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Cause, Context, Effect, Layer, Scope } from "effect"
|
||||
import { Cause, Context, Effect, Exit, Layer, Scope } from "effect"
|
||||
|
||||
const log = Log.create({ service: "kilocode-watcher" })
|
||||
|
||||
@@ -17,13 +19,8 @@ export namespace KilocodeWatcher {
|
||||
|
||||
// Embedded editor clients (VS Code, JetBrains) have their own file watching
|
||||
// and git integration and do not consume the CLI's vcs.branch.updated event,
|
||||
// so they must not eagerly warm the location stack — that starts a native
|
||||
// @parcel/watcher subscription per instance that lives for the whole session.
|
||||
// On macOS FSEvents watches the entire subtree recursively (the ignore list
|
||||
// is only a userspace filter), so an always-on, consumer-less watcher on a
|
||||
// churny workspace burns CPU and leaks native memory while idle. The
|
||||
// standalone CLI/TUI stays eager because its sidebar branch label is the only
|
||||
// consumer and no request-driven route would otherwise build the stack.
|
||||
// so they must not eagerly warm the location stack. The standalone CLI/TUI
|
||||
// keeps this subscription for live branch-label updates.
|
||||
export function eager(client = Flag.KILO_CLIENT) {
|
||||
return client !== "vscode" && client !== "jetbrains"
|
||||
}
|
||||
@@ -33,31 +30,47 @@ export namespace KilocodeWatcher {
|
||||
Effect.gen(function* () {
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const active = new Map<string, Scope.Closeable>()
|
||||
const ref = (directory: string) => Location.Ref.make({ directory: AbsolutePath.make(directory) })
|
||||
|
||||
const state = yield* InstanceState.make(
|
||||
Effect.fn("KilocodeWatcher.state")(function* (ctx) {
|
||||
if (ctx.project.vcs !== "git") return
|
||||
// Warm the v2 location stack for this instance and hold it for the
|
||||
// instance lifetime. Its Watcher subscribes to .git so Vcs sees HEAD
|
||||
// changes and publishes vcs.branch.updated in the CLI, where no v2
|
||||
// route would otherwise build the stack. The ref must be built the
|
||||
// same way the file/pty handlers build theirs (Location.Ref.make) so
|
||||
// the LayerMap shares a single build per directory.
|
||||
const ref = Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) })
|
||||
yield* locations.contextEffect(ref)
|
||||
// Tear the stack down with the instance instead of letting it idle
|
||||
// in the LayerMap; same pattern as the pty handlers' disposer.
|
||||
yield* Effect.addFinalizer(() => locations.invalidate(ref).pipe(Effect.ignore))
|
||||
}),
|
||||
const off = registerDisposer((directory) =>
|
||||
Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const child = active.get(directory)
|
||||
if (child) {
|
||||
active.delete(directory)
|
||||
yield* Scope.close(child, Exit.void)
|
||||
}
|
||||
yield* locations.invalidate(ref(directory))
|
||||
}).pipe(Effect.ignore),
|
||||
),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(off))
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.forEach(active.values(), (child) => Scope.close(child, Exit.void), { discard: true }).pipe(
|
||||
Effect.andThen(Effect.sync(() => active.clear())),
|
||||
),
|
||||
)
|
||||
|
||||
const warm = (ctx: InstanceContext, child: Scope.Closeable) =>
|
||||
Scope.provide(child)(locations.contextEffect(ref(ctx.directory)))
|
||||
|
||||
return Service.of({
|
||||
init: Effect.fn("KilocodeWatcher.init")(function* () {
|
||||
yield* InstanceState.get(state).pipe(
|
||||
const ctx = yield* InstanceState.context
|
||||
if (ctx.project.vcs !== "git" || active.has(ctx.directory)) return
|
||||
|
||||
const child = yield* Scope.make()
|
||||
active.set(ctx.directory, child)
|
||||
yield* warm(ctx, child).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.sync(() => log.warn("instance watcher init failed", { err: Cause.squash(cause) })),
|
||||
Effect.gen(function* () {
|
||||
if (active.get(ctx.directory) === child) active.delete(ctx.directory)
|
||||
yield* Scope.close(child, Exit.void).pipe(Effect.ignore)
|
||||
yield* Effect.sync(() => log.warn("instance watcher init failed", { err: Cause.squash(cause) }))
|
||||
}),
|
||||
),
|
||||
Effect.forkIn(scope),
|
||||
Effect.forkIn(scope, { startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -23,6 +23,9 @@ Skip when:
|
||||
|
||||
## Rules
|
||||
- Update status in real time; don't batch completions
|
||||
- After creating a list, call this tool before starting the first item.
|
||||
- After completing each item, call this tool before starting the next item, with the completed item marked `completed` and the next item marked `in_progress`.
|
||||
- Do not complete multiple items or continue through multiple steps without updating the list between them.
|
||||
- Mark `completed` only after the required work is actually done, including any required verification. Never based on intent.
|
||||
- Keep exactly one `in_progress` while work remains
|
||||
- If blocked or partial, keep it `in_progress` and add a follow-up todo describing the blocker
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Deferred, Effect, Fiber, Layer } from "effect"
|
||||
import { Context, Deferred, Effect, Fiber, Layer, LayerMap } from "effect"
|
||||
import * as TestConsole from "effect/testing/TestConsole"
|
||||
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { disposeInstance } from "../../src/effect/instance-registry"
|
||||
import { Git } from "../../src/git"
|
||||
import { InstanceBootstrap } from "../../src/project/bootstrap"
|
||||
import { InstanceStore } from "../../src/project/instance-store"
|
||||
import { KilocodeWatcher } from "../../src/kilocode/watcher"
|
||||
import type { InstanceContext } from "../../src/project/instance-context"
|
||||
import { tmpdirScoped } from "../fixture/fixture"
|
||||
import { awaitWithTimeout, testEffect } from "../lib/effect"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap, type LocationServices } from "@opencode-ai/core/location-services"
|
||||
|
||||
const layer = Layer.mergeAll(
|
||||
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
|
||||
@@ -44,47 +50,104 @@ describe("KilocodeWatcher.eager", () => {
|
||||
})
|
||||
})
|
||||
|
||||
live("instances publish branch updates after git switch", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const git = yield* Git.Service
|
||||
const store = yield* InstanceStore.Service
|
||||
const current = yield* git.branch(dir)
|
||||
if (!current) return yield* Effect.die("missing initial branch")
|
||||
live(
|
||||
"instances publish branch updates after git switch",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const git = yield* Git.Service
|
||||
const store = yield* InstanceStore.Service
|
||||
const current = yield* git.branch(dir)
|
||||
if (!current) return yield* Effect.die("missing initial branch")
|
||||
|
||||
const branch = `watch-${Math.random().toString(36).slice(2)}`
|
||||
const created = yield* git.run(["branch", branch], { cwd: dir })
|
||||
expect(created.exitCode).toBe(0)
|
||||
yield* store.load({ directory: dir })
|
||||
const branch = `watch-${Math.random().toString(36).slice(2)}`
|
||||
const created = yield* git.run(["branch", branch], { cwd: dir })
|
||||
expect(created.exitCode).toBe(0)
|
||||
yield* store.load({ directory: dir })
|
||||
|
||||
const pending = yield* Deferred.make<string | undefined>()
|
||||
const handler = (event: GlobalEvent) => {
|
||||
if (event.directory !== dir || event.payload.type !== "vcs.branch.updated") return
|
||||
if (event.payload.properties.branch !== branch) return
|
||||
Deferred.doneUnsafe(pending, Effect.succeed(event.payload.properties.branch))
|
||||
}
|
||||
GlobalBus.on("event", handler)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", handler)))
|
||||
|
||||
// The watcher exposes no readiness signal (its .git subscription is forked
|
||||
// during instance warm-up), so keep generating HEAD churn in the background
|
||||
// and synchronize on the event itself with the full test budget.
|
||||
const churn = yield* Effect.gen(function* () {
|
||||
while (true) {
|
||||
yield* git.run(["switch", current], { cwd: dir })
|
||||
yield* Effect.sleep("50 millis")
|
||||
yield* git.run(["switch", branch], { cwd: dir })
|
||||
yield* Effect.sleep("100 millis")
|
||||
const pending = yield* Deferred.make<string | undefined>()
|
||||
const handler = (event: GlobalEvent) => {
|
||||
if (event.directory !== dir || event.payload.type !== "vcs.branch.updated") return
|
||||
if (event.payload.properties.branch !== branch) return
|
||||
Deferred.doneUnsafe(pending, Effect.succeed(event.payload.properties.branch))
|
||||
}
|
||||
}).pipe(Effect.forkScoped)
|
||||
GlobalBus.on("event", handler)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", handler)))
|
||||
|
||||
const updated = yield* awaitWithTimeout(
|
||||
Deferred.await(pending),
|
||||
"timed out waiting for vcs.branch.updated",
|
||||
"15 seconds",
|
||||
)
|
||||
yield* Fiber.interrupt(churn)
|
||||
expect(updated).toBe(branch)
|
||||
}),
|
||||
// The watcher exposes no readiness signal (its .git subscription is forked
|
||||
// during instance warm-up), so keep generating HEAD churn in the background
|
||||
// and synchronize on the event itself with the full test budget.
|
||||
const churn = yield* Effect.gen(function* () {
|
||||
while (true) {
|
||||
yield* git.run(["switch", current], { cwd: dir })
|
||||
yield* Effect.sleep("50 millis")
|
||||
yield* git.run(["switch", branch], { cwd: dir })
|
||||
yield* Effect.sleep("100 millis")
|
||||
}
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
const updated = yield* awaitWithTimeout(
|
||||
Deferred.await(pending),
|
||||
"timed out waiting for vcs.branch.updated",
|
||||
"15 seconds",
|
||||
)
|
||||
yield* Fiber.interrupt(churn)
|
||||
expect(updated).toBe(branch)
|
||||
}),
|
||||
20_000,
|
||||
)
|
||||
|
||||
test.serial(
|
||||
"isolates location lifetimes between instances",
|
||||
async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const one = yield* tmpdirScoped()
|
||||
const two = yield* tmpdirScoped()
|
||||
const warmed = new Map<string, number>()
|
||||
const invalidated: string[] = []
|
||||
const map = yield* LayerMap.make((ref: Location.Ref) =>
|
||||
Layer.effectContext(
|
||||
Effect.acquireRelease(
|
||||
Effect.sync(() => {
|
||||
warmed.set(ref.directory, (warmed.get(ref.directory) ?? 0) + 1)
|
||||
return Context.empty() as Context.Context<LocationServices>
|
||||
}),
|
||||
() => Effect.sync(() => invalidated.push(ref.directory)),
|
||||
),
|
||||
),
|
||||
)
|
||||
const watcher = KilocodeWatcher.layer.pipe(Layer.provide(Layer.succeed(LocationServiceMap.Service, map)))
|
||||
const services = yield* Layer.build(watcher)
|
||||
const init = (directory: string) =>
|
||||
KilocodeWatcher.Service.use((service) => service.init()).pipe(
|
||||
Effect.provide(services),
|
||||
Effect.provideService(InstanceRef, {
|
||||
directory,
|
||||
worktree: directory,
|
||||
project: { vcs: "git" },
|
||||
} as InstanceContext),
|
||||
)
|
||||
|
||||
yield* init(one)
|
||||
yield* init(one)
|
||||
yield* init(two)
|
||||
yield* Effect.yieldNow
|
||||
expect(warmed).toEqual(
|
||||
new Map([
|
||||
[one, 1],
|
||||
[two, 1],
|
||||
]),
|
||||
)
|
||||
yield* Effect.promise(() => disposeInstance(one))
|
||||
expect(invalidated).toEqual([one])
|
||||
yield* Effect.promise(() => disposeInstance(two))
|
||||
expect(invalidated).toEqual([one, two])
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(Layer.mergeAll(AppNodeBuilder.build(CrossSpawnSpawner.node), TestConsole.layer)),
|
||||
),
|
||||
)
|
||||
},
|
||||
20_000,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import DESCRIPTION_WRITE from "../../src/tool/todowrite.txt"
|
||||
|
||||
describe("todowrite description", () => {
|
||||
test("requires an update between each task", () => {
|
||||
expect(DESCRIPTION_WRITE).toContain("call this tool before starting the first item")
|
||||
expect(DESCRIPTION_WRITE).toContain("After completing each item, call this tool before starting the next item")
|
||||
expect(DESCRIPTION_WRITE).toContain("Do not complete multiple items or continue through multiple steps")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { createKiloClient } from "@kilocode/sdk/v2"
|
||||
import { Effect } from "effect"
|
||||
import { cliIt } from "../lib/cli-process"
|
||||
|
||||
const todos = [
|
||||
[
|
||||
{ content: "Inspect files", status: "in_progress", priority: "high" },
|
||||
{ content: "Implement fix", status: "pending", priority: "medium" },
|
||||
{ content: "Run checks", status: "pending", priority: "medium" },
|
||||
{ content: "Report results", status: "pending", priority: "low" },
|
||||
],
|
||||
[
|
||||
{ content: "Inspect files", status: "completed", priority: "high" },
|
||||
{ content: "Implement fix", status: "in_progress", priority: "medium" },
|
||||
{ content: "Run checks", status: "pending", priority: "medium" },
|
||||
{ content: "Report results", status: "pending", priority: "low" },
|
||||
],
|
||||
[
|
||||
{ content: "Inspect files", status: "completed", priority: "high" },
|
||||
{ content: "Implement fix", status: "completed", priority: "medium" },
|
||||
{ content: "Run checks", status: "in_progress", priority: "medium" },
|
||||
{ content: "Report results", status: "pending", priority: "low" },
|
||||
],
|
||||
[
|
||||
{ content: "Inspect files", status: "completed", priority: "high" },
|
||||
{ content: "Implement fix", status: "completed", priority: "medium" },
|
||||
{ content: "Run checks", status: "completed", priority: "medium" },
|
||||
{ content: "Report results", status: "in_progress", priority: "low" },
|
||||
],
|
||||
[
|
||||
{ content: "Inspect files", status: "completed", priority: "high" },
|
||||
{ content: "Implement fix", status: "completed", priority: "medium" },
|
||||
{ content: "Run checks", status: "completed", priority: "medium" },
|
||||
{ content: "Report results", status: "completed", priority: "low" },
|
||||
],
|
||||
] as const
|
||||
|
||||
describe("todowrite end-to-end", () => {
|
||||
cliIt.live(
|
||||
"persists every sequential update through the real CLI session",
|
||||
({ llm, opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const server = yield* opencode.serve()
|
||||
const client = createKiloClient({ baseUrl: server.url })
|
||||
const session = yield* Effect.promise(() =>
|
||||
client.session.create({
|
||||
permission: [{ permission: "*", action: "allow", pattern: "*" }],
|
||||
}),
|
||||
)
|
||||
const sessionID = session.data?.id
|
||||
if (!sessionID) throw new Error("test session was not created")
|
||||
|
||||
for (const list of todos) yield* llm.tool("todowrite", { todos: list })
|
||||
yield* llm.text("done")
|
||||
|
||||
const result = yield* opencode.run("complete this four-step task", {
|
||||
extraArgs: ["--attach", server.url, "--session", sessionID, "--auto"],
|
||||
timeoutMs: 90_000,
|
||||
})
|
||||
opencode.expectExit(result, 0)
|
||||
|
||||
expect(JSON.stringify(yield* llm.inputs)).toContain(
|
||||
"After completing each item, call this tool before starting the next item",
|
||||
)
|
||||
const saved = yield* Effect.promise(() => client.session.todo({ sessionID }))
|
||||
const final = todos[todos.length - 1]?.map((todo) => ({ ...todo }))
|
||||
expect(saved.data).toEqual(final)
|
||||
|
||||
const messages = yield* Effect.promise(() => client.session.messages({ sessionID }))
|
||||
const calls =
|
||||
messages.data?.flatMap((message) =>
|
||||
message.parts.filter((part) => part.type === "tool" && part.tool === "todowrite"),
|
||||
) ?? []
|
||||
expect(calls).toHaveLength(todos.length)
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user