fix(opencode): address fourth-pass upstream review

This commit is contained in:
Johnny Amancio
2026-07-16 15:32:19 +02:00
parent d4fc1f0112
commit 1872dbc25b
13 changed files with 750 additions and 32 deletions
+5
View File
@@ -125,6 +125,11 @@ export const layer = Layer.effect(
// 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()
yield* KiloReference.sync({
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) }))))
const whitelistedDirs = [
+18 -1
View File
@@ -131,6 +131,22 @@ export function ensure(cache: RepositoryCache.Interface, item: Extract<Resolved,
)
}
function same(left: Reference.Source | undefined, right: Reference.Source) {
if (!left || left.type !== right.type) return false
if (left.type === "local" && right.type === "local") {
return left.path === right.path && left.description === right.description && left.hidden === right.hidden
}
if (left.type === "git" && right.type === "git") {
return (
left.repository === right.repository &&
left.branch === right.branch &&
left.description === right.description &&
left.hidden === right.hidden
)
}
return false
}
// Keep Core V2 tools on the same effective Kilo config used by stable tools. Core's standalone
// scanner cannot see account/managed config or KILO_CONFIG_CONTENT, so replace its provisional
// references after both config systems finish booting.
@@ -141,7 +157,6 @@ export const sync = Effect.fn("KiloReference.sync")(function* (input: {
}) {
const service = yield* Reference.Service
const entries = resolveAll(input)
if (entries.length === 0 && (yield* service.list()).length === 0) return
const sources = entries.flatMap<readonly [string, Reference.Source]>((item) => {
if (item.kind === "invalid") return []
if (item.kind === "local") {
@@ -170,5 +185,7 @@ export const sync = Effect.fn("KiloReference.sync")(function* (input: {
] as const,
]
})
const current = new Map((yield* service.list()).map((item) => [item.name, item.source]))
if (current.size === sources.length && sources.every(([name, source]) => same(current.get(name), source))) return
yield* service.replace(sources)
})
@@ -3,30 +3,47 @@ import { InstanceRef } from "@/effect/instance-ref"
import { isInterrupted } from "@/kilocode/effect/cause"
import * as KiloReference from "@/kilocode/reference"
import { InstanceStore } from "@/project/instance-store"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Location } from "@opencode-ai/core/location"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { ReferenceReconciler } from "@opencode-ai/server/kilocode/reference-reconciler"
import { Effect, Layer } from "effect"
export const layer = Layer.effect(
ReferenceReconciler,
const reconcile = Effect.gen(function* () {
const config = yield* Config.Service
const store = yield* InstanceStore.Service
return Effect.gen(function* () {
const location = yield* Location.Service
const ctx = yield* store.load({ directory: location.directory })
const cfg = yield* config.get().pipe(Effect.provideService(InstanceRef, ctx))
yield* PluginBoot.Service.use((boot) => boot.wait())
yield* KiloReference.sync({
references: cfg.references ?? cfg.reference ?? {},
directory: ctx.directory,
worktree: ctx.worktree,
}).pipe(
Effect.catchCause((cause) =>
isInterrupted(cause) ? Effect.interrupt : Effect.logWarning("reference sync failed", { cause }),
),
)
})
})
export const layer = Layer.effect(ReferenceReconciler, reconcile)
export const locations = Layer.effect(
LocationServiceMap,
Effect.gen(function* () {
const config = yield* Config.Service
const store = yield* InstanceStore.Service
return Effect.gen(function* () {
const location = yield* Location.Service
const ctx = yield* store.load({ directory: location.directory })
const cfg = yield* config.get().pipe(Effect.provideService(InstanceRef, ctx))
yield* PluginBoot.Service.use((boot) => boot.wait())
yield* KiloReference.sync({
references: cfg.references ?? cfg.reference ?? {},
directory: ctx.directory,
worktree: ctx.worktree,
}).pipe(
Effect.catchCause((cause) =>
isInterrupted(cause) ? Effect.interrupt : Effect.logWarning("reference sync failed", { cause }),
),
)
const locations = yield* LocationServiceMap
const initialize = yield* reconcile
return LocationServiceMap.of({
...locations,
get: (ref) => Layer.effectDiscard(initialize).pipe(Layer.provideMerge(locations.get(ref))),
contextEffect: (ref) =>
Effect.gen(function* () {
const context = yield* locations.contextEffect(ref)
yield* initialize.pipe(Effect.provide(context))
return context
}),
})
}),
)
).pipe(Layer.provide(LocationServiceMap.layer))
@@ -102,7 +102,10 @@ import { sessionHandlers } from "./handlers/session"
import { syncHandlers } from "./handlers/sync"
import { tuiHandlers } from "./handlers/tui"
import { handlers } from "@opencode-ai/server/handlers"
import { layer as referenceReconcilerLayer } from "@/kilocode/server/reference-reconciler" // kilocode_change
import {
layer as referenceReconcilerLayer,
locations as locationServiceMapLayer,
} from "@/kilocode/server/reference-reconciler" // kilocode_change
import { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error"
import { workspaceHandlers } from "./handlers/workspace"
// kilocode_change start
@@ -181,7 +184,9 @@ const instanceRoutes = instanceApiRoutes.pipe(
Layer.provide([httpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]),
)
const serverRoutes = HttpApiBuilder.layer(Api).pipe(
Layer.provide(handlers.pipe(Layer.provide(referenceReconcilerLayer))), // kilocode_change
// kilocode_change start - effective references must be ready before any V2 location consumer runs
Layer.provide(handlers.pipe(Layer.provide(locationServiceMapLayer), Layer.provide(referenceReconcilerLayer))),
// kilocode_change end
Layer.provide([serverHttpApiAuthLayer, v2SchemaErrorLayer]),
)
+15 -2
View File
@@ -29,6 +29,8 @@ import SOUL from "../kilocode/soul.txt"
import type { EditorContext } from "../kilocode/editor-context"
import { KilocodeSystemPrompt } from "../kilocode/system-prompt"
import { isLing } from "../kilocode/model-match"
import { Config } from "@/config/config"
import * as KiloReference from "@/kilocode/reference"
// kilocode_change end
// kilocode_change start
@@ -97,6 +99,7 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const skill = yield* Skill.Service
const locations = yield* LocationServiceMap
const config = yield* Config.Service // kilocode_change
return Service.of({
// kilocode_change start
@@ -105,8 +108,14 @@ export const layer = Layer.effect(
editorContext?: EditorContext,
) {
const ctx = yield* InstanceState.context
const cfg = yield* config.get()
const references = yield* Effect.gen(function* () {
yield* (yield* PluginBoot.Service).wait()
yield* KiloReference.sync({
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) }))))
return [
@@ -150,10 +159,14 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer), Layer.provide(LocationServiceMap.layer))
export const defaultLayer = layer.pipe(
Layer.provide(Skill.defaultLayer),
Layer.provide(LocationServiceMap.layer),
Layer.provide(Config.defaultLayer), // kilocode_change
)
const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, [])
export const node = LayerNode.make(layer, [Skill.node, locationServiceMapNode])
export const node = LayerNode.make(layer, [Skill.node, locationServiceMapNode, Config.node]) // kilocode_change
export * as SystemPrompt from "./system"
@@ -6,6 +6,12 @@ import * as Reference from "../../src/kilocode/reference"
import { Reference as CoreReference } from "@opencode-ai/core/reference"
import { EventV2 } from "@opencode-ai/core/event"
import { Global } from "@opencode-ai/core/global"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Config } from "../../src/config/config"
import { locations } from "../../src/kilocode/server/reference-reconciler"
import { testInstanceStoreLayer, tmpdir } from "../fixture/fixture"
function remote() {
const item = Reference.resolveAll({
@@ -67,4 +73,66 @@ describe("configured references", () => {
}),
])
})
test("sync does not publish an update for equivalent references", async () => {
const cache = Layer.mock(RepositoryCache.Service, {
ensure: () => Effect.die("unexpected Git materialization"),
})
const updates: string[] = []
const events = Layer.mock(EventV2.Service)({
publish: (definition, data) => {
updates.push(definition.type)
return Effect.succeed({ id: EventV2.ID.make(`evt_${updates.length}`), type: definition.type, data })
},
})
const layer = CoreReference.layer.pipe(
Layer.provide(cache),
Layer.provide(events),
Layer.provide(Global.defaultLayer),
)
const input = {
references: { docs: { path: "./docs", description: "Internal documentation", hidden: true } },
directory: "/workspace/src",
worktree: "/workspace",
}
await Effect.runPromise(
Effect.gen(function* () {
yield* Reference.sync(input)
yield* Reference.sync(input)
}).pipe(Effect.provide(layer), Effect.scoped),
)
expect(updates).toEqual(["reference.updated"])
})
test("initializes effective references before exposing location services", async () => {
await using tmp = await tmpdir({
config: {
formatter: false,
lsp: false,
references: {
docs: { path: "./docs", description: "Internal documentation" },
},
},
})
const layer = locations.pipe(Layer.provide(Config.defaultLayer), Layer.provide(testInstanceStoreLayer))
const result = await Effect.runPromise(
Effect.gen(function* () {
const map = yield* LocationServiceMap
return yield* CoreReference.Service.use((reference) => reference.list()).pipe(
Effect.provide(map.get(Location.Ref.make({ directory: AbsolutePath.make(tmp.path) }))),
)
}).pipe(Effect.provide(layer), Effect.scoped),
)
expect(result).toEqual([
expect.objectContaining({
name: "docs",
path: path.join(tmp.path, "docs"),
description: "Internal documentation",
}),
])
}, 15_000)
})
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { createLeadingTrailingSignal } from "@/kilocode/plugins/session-switcher/preview-pane"
import { createDebouncedSignal } from "@tui/util/signal"
describe("TUI scheduling", () => {
@@ -16,4 +17,24 @@ 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()
})
})
})
@@ -0,0 +1,568 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import type { Event, GlobalEvent } from "@kilocode/sdk/v2"
import { onMount } from "solid-js"
import { ProjectProvider, useProject } from "@tui/context/project"
import { SDKProvider } from "@tui/context/sdk"
import { SyncProviderV2, useSyncV2 } from "@/kilocode/plugins/sync-v2"
import { createEventSource, createFetch, directory, json } from "../../../../tui/test/fixture/tui-sdk"
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
while (!fn()) {
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
await Bun.sleep(10)
}
}
// kilocode_change start - live events are filtered by the resolved project ID
function synced(ready: () => void) {
const project = useProject()
onMount(async () => {
await project.sync()
ready()
})
}
// kilocode_change end
function global(payload: Event): GlobalEvent {
return { directory, project: "proj_test", payload }
}
function emitTwice(events: ReturnType<typeof createEventSource>, payload: Event) {
const event = global(payload)
events.emit(event)
events.emit(event)
}
test("sync v2 settles pending tools when a live failure arrives", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
synced(ready)
return <box />
}
const app = await testRender(() => (
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_agent_1",
type: "session.next.agent.switched",
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
})
emitTwice(events, {
id: "evt_model_1",
type: "session.next.model.switched",
properties: {
sessionID: "session-1",
messageID: "msg_model_1",
timestamp: 0,
model: { id: "model-1", providerID: "provider-1" },
},
})
emitTwice(events, {
id: "evt_step_started_1",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
timestamp: 1,
agent: "build",
model: { id: "model-1", providerID: "provider-1" },
},
})
emitTwice(events, {
id: "evt_input_1",
type: "session.next.tool.input.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
timestamp: 2,
callID: "call-1",
name: "bash",
},
})
emitTwice(events, {
id: "evt_called_1",
type: "session.next.tool.called",
properties: {
sessionID: "session-1",
timestamp: 2,
assistantMessageID: "msg_explicit_assistant_9",
callID: "call-1",
tool: "bash",
input: {},
provider: { executed: false, metadata: { fake: { call: true } } },
},
})
emitTwice(events, {
id: "evt_failed_1",
type: "session.next.tool.failed",
properties: {
sessionID: "session-1",
timestamp: 3,
assistantMessageID: "msg_explicit_assistant_9",
callID: "call-1",
error: { type: "unknown", message: "aborted" },
provider: { executed: false, metadata: { fake: { result: true } } },
},
})
await wait(() => {
const assistant = sync.session.message.fromSession("session-1")[0]
return (
assistant?.type === "assistant" &&
assistant.content[0]?.type === "tool" &&
assistant.content[0].state.status === "error"
)
})
const assistant = sync.session.message.fromSession("session-1")[0]
expect(assistant?.type).toBe("assistant")
if (assistant?.type !== "assistant") return
expect(assistant.id).toBe("msg_explicit_assistant_9")
const tool = assistant.content[0]
expect(tool?.type).toBe("tool")
if (tool?.type !== "tool") return
expect(tool.state.status).toBe("error")
if (tool.state.status !== "error") return
expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" })
expect(tool.state.input).toEqual({})
expect(tool.state.structured).toEqual({})
expect(tool.state.content).toEqual([])
expect(tool.provider).toEqual({
executed: false,
metadata: { fake: { call: true } },
resultMetadata: { fake: { result: true } },
})
expect(sync.session.message.fromSession("session-1").map((message) => message.type)).toEqual([
"assistant",
"model-switched",
"agent-switched",
])
} finally {
app.renderer.destroy()
}
})
test("sync v2 renders admitted prompts only after promotion", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
synced(ready)
return <box />
}
const app = await testRender(() => (
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_admitted_1",
type: "session.next.prompt.admitted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 0,
prompt: { text: "hello" },
delivery: "steer",
},
})
expect(sync.session.message.fromSession("session-1")).toEqual([])
emitTwice(events, {
id: "evt_promoted_1",
type: "session.next.prompt.promoted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 1,
prompt: { text: "hello" },
timeCreated: 0,
},
})
await wait(() => sync.session.message.fromSession("session-1").length === 1)
const message = sync.session.message.fromSession("session-1")[0]
expect(message?.type).toBe("user")
if (message?.type !== "user") return
expect(message).toMatchObject({ id: "msg_user_1", text: "hello" })
} finally {
app.renderer.destroy()
}
})
test("sync v2 renders a promoted prompt when admission was missed", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
synced(ready)
return <box />
}
const app = await testRender(() => (
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_promoted_1",
type: "session.next.prompt.promoted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 1,
prompt: { text: "hello" },
timeCreated: 0,
},
})
await wait(() => sync.session.message.fromSession("session-1").length === 1)
expect(sync.session.message.fromSession("session-1")[0]?.id).toBe("msg_user_1")
} finally {
app.renderer.destroy()
}
})
test("sync v2 projects live context updates with their message ID", async () => {
const events = createEventSource()
const calls = createFetch()
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
synced(ready)
return <box />
}
const app = await testRender(() => (
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_context_1",
type: "session.next.context.updated",
properties: {
sessionID: "session-1",
messageID: "msg_context_1",
timestamp: 1,
text: "Updated context",
},
})
await wait(() => sync.session.message.fromSession("session-1").length === 1)
expect(sync.session.message.fromSession("session-1")[0]).toMatchObject({
id: "msg_context_1",
type: "system",
text: "Updated context",
})
} finally {
app.renderer.destroy()
}
})
test("sync v2 preserves live events while snapshot hydration is in flight", async () => {
const events = createEventSource()
const response = Promise.withResolvers<Response>()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-1/message") return response.promise
return undefined
})
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
synced(ready)
return <box />
}
const app = await testRender(() => (
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
))
try {
await mounted
const hydration = sync.session.message.sync("session-1")
emitTwice(events, {
id: "evt_agent_1",
type: "session.next.agent.switched",
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
})
response.resolve(json({ data: [] }))
await hydration
expect(sync.session.message.fromSession("session-1").map((message) => [message.id, message.type])).toEqual([
["msg_agent_1", "agent-switched"],
])
} finally {
app.renderer.destroy()
}
})
test("sync v2 replaces stale cached rows while preserving in-flight live rows", async () => {
const events = createEventSource()
const response = Promise.withResolvers<Response>()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-1/message") return response.promise
return undefined
})
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
synced(ready)
return <box />
}
const app = await testRender(() => (
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_promoted_1",
type: "session.next.prompt.promoted",
properties: {
sessionID: "session-1",
messageID: "msg_user_1",
timestamp: 1,
prompt: { text: "stale" },
timeCreated: 0,
},
})
await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_user_1")
const hydration = sync.session.message.sync("session-1")
emitTwice(events, {
id: "evt_agent_1",
type: "session.next.agent.switched",
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 2, agent: "build" },
})
await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_agent_1")
response.resolve(
json({
data: [{ id: "msg_user_1", type: "user", text: "fresh", time: { created: 0 } }],
}),
)
await hydration
expect(sync.session.message.fromSession("session-1").map((message) => [message.id, message.type])).toEqual([
["msg_agent_1", "agent-switched"],
["msg_user_1", "user"],
])
expect(sync.session.message.fromSession("session-1")[1]).toMatchObject({ text: "fresh" })
} finally {
app.renderer.destroy()
}
})
test("sync v2 preserves snapshot order and metadata for in-flight updates", async () => {
const events = createEventSource()
const response = Promise.withResolvers<Response>()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-1/message") return response.promise
return undefined
})
let sync!: ReturnType<typeof useSyncV2>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useSyncV2()
synced(ready)
return <box />
}
const app = await testRender(() => (
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
<Probe />
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
))
try {
await mounted
emitTwice(events, {
id: "evt_step_older",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_older",
timestamp: 0,
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
emitTwice(events, {
id: "evt_step_1",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_old",
timestamp: 1,
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_assistant_old")
const hydration = sync.session.message.sync("session-1")
emitTwice(events, {
id: "evt_text_1",
type: "session.next.text.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_old",
timestamp: 2,
textID: "text-1",
},
})
emitTwice(events, {
id: "evt_text_older",
type: "session.next.text.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_older",
timestamp: 2,
textID: "text-older",
},
})
await wait(() => {
const messages = sync.session.message.fromSession("session-1")
return messages.every((message) => message.type !== "assistant" || message.content[0]?.type === "text")
})
response.resolve(
json({
data: [
{
id: "msg_assistant_new",
type: "assistant",
agent: "build",
model: { id: "model", providerID: "provider" },
content: [],
time: { created: 3 },
},
{
id: "msg_assistant_old",
type: "assistant",
metadata: { source: "snapshot" },
agent: "build",
model: { id: "model", providerID: "provider" },
content: [],
time: { created: 1 },
},
],
}),
)
await hydration
emitTwice(events, {
id: "evt_step_late_duplicate",
type: "session.next.step.started",
properties: {
sessionID: "session-1",
assistantMessageID: "msg_assistant_old",
timestamp: 1,
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
expect(sync.session.message.fromSession("session-1").map((message) => message.id)).toEqual([
"msg_assistant_new",
"msg_assistant_old",
"msg_assistant_older",
])
expect(JSON.parse(JSON.stringify(sync.session.message.fromSession("session-1")[1]))).toMatchObject({
metadata: { source: "snapshot" },
content: [{ type: "text", id: "text-1", text: "" }],
})
expect(JSON.parse(JSON.stringify(sync.session.message.fromSession("session-1")[2]))).toMatchObject({
content: [{ type: "text", id: "text-older", text: "" }],
})
} finally {
app.renderer.destroy()
}
})
@@ -7,6 +7,7 @@ import { Permission } from "../../src/permission"
import { SystemPrompt } from "../../src/session/system"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { testEffect } from "../lib/effect"
import { Config } from "../../src/config/config" // kilocode_change
const skills: Skill.Info[] = [
{
@@ -44,6 +45,7 @@ const build: Agent.Info = {
const it = testEffect(
SystemPrompt.layer.pipe(
Layer.provide(LocationServiceMap.layer),
Layer.provide(Config.defaultLayer), // kilocode_change
Layer.provide(
Layer.succeed(
Skill.Service,
+1 -2
View File
@@ -1,5 +1,4 @@
import { SessionV2 } from "@opencode-ai/core/session"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { Layer } from "effect"
import { layer as locationLayer } from "./groups/location"
@@ -43,5 +42,5 @@ export const handlers = Layer.mergeAll(
Layer.provide(SessionV2.defaultLayer),
Layer.provide(SessionExecutionLocal.defaultLayer),
Layer.provide(PermissionSaved.defaultLayer),
Layer.provide(LocationServiceMap.layer),
// kilocode_change - the host provides LocationServiceMap so Kilo can install effective-reference initialization
)
@@ -1,13 +1,14 @@
import { Location } from "@opencode-ai/core/location"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { Reference } from "@opencode-ai/core/reference"
import { Context, Effect } from "effect"
import { Context, Effect, Layer } from "effect"
export const ReferenceReconciler = Context.Reference<
export class ReferenceReconciler extends Context.Service<
ReferenceReconciler,
Effect.Effect<void, never, Location.Service | PluginBoot.Service | Reference.Service>
>("@kilocode/ReferenceReconciler", {
defaultValue: () => Effect.void,
})
>()("@kilocode/ReferenceReconciler") {}
export const noop = Layer.succeed(ReferenceReconciler, Effect.void)
export function reconcile<A, E, R>(effect: Effect.Effect<A, E, R>) {
return Effect.flatMap(ReferenceReconciler, (reconciler) => Effect.andThen(reconciler, effect))
+2
View File
@@ -9,10 +9,12 @@ import { ServerAuth } from "./auth"
import { handlers } from "./handlers"
import { authorizationLayer } from "./middleware/authorization"
import { schemaErrorLayer } from "./middleware/schema-error"
import { noop as referenceNoop } from "./kilocode/reference-reconciler" // kilocode_change
export function createRoutes(password?: string) {
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers),
Layer.provide(referenceNoop), // kilocode_change - standalone server has no Kilo config reconciler
Layer.provide(authorizationLayer),
Layer.provide(schemaErrorLayer),
Layer.provide(
+1 -1
View File
@@ -1,4 +1,4 @@
// kilocode_change new file
// kilocode_change - new file
import { RGBA } from "@opentui/core"
import { For, type JSX } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"