mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix: harden OpenCode v1.14.48 integration
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Preserve image attachments when Photon is unavailable, enforce attachment limits for user images, and correlate shell lifecycle events correctly.
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.13",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
|
||||
+22
@@ -1,5 +1,6 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import ai.kilocode.backend.migration.session.LegacySessionIds
|
||||
import ai.kilocode.jetbrains.api.infrastructure.Serializer
|
||||
import ai.kilocode.jetbrains.api.model.Session
|
||||
import ai.kilocode.jetbrains.api.model.SessionStatus
|
||||
@@ -40,6 +41,27 @@ class SessionModelSerializationTest {
|
||||
assertNull(obj.summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Session decodes canonical migrated and unprefixed legacy IDs`() {
|
||||
val src = """{
|
||||
"id": "ses_abc",
|
||||
"slug": "canonical",
|
||||
"projectID": "prj_123",
|
||||
"directory": "/test/project",
|
||||
"title": "Canonical",
|
||||
"version": "1.0.0",
|
||||
"time": {"created": 1000, "updated": 2000}
|
||||
}"""
|
||||
val migrated = LegacySessionIds.createSessionId("task-abc")
|
||||
val canonical = json.decodeFromString<Session>(src)
|
||||
val imported = json.decodeFromString<Session>(src.replace("ses_abc", migrated))
|
||||
val legacy = json.decodeFromString<Session>(src.replace("ses_abc", "s1"))
|
||||
|
||||
assertEquals("ses_abc", canonical.id)
|
||||
assertEquals(migrated, imported.id)
|
||||
assertEquals("s1", legacy.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Session with summary`() {
|
||||
val src = """{
|
||||
|
||||
@@ -345,7 +345,7 @@ export const layer = Layer.effect(
|
||||
`Repository: ${reference.repository}`,
|
||||
...(reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
|
||||
`Cached directory: ${reference.path}`,
|
||||
`OpenCode materializes this configured repository before use. Do not call repo_clone for this reference.`, // kilocode_change
|
||||
`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")
|
||||
|
||||
@@ -70,6 +70,7 @@ import { createTuiApi } from "@/cli/cmd/tui/plugin/api"
|
||||
import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
|
||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||
import { kitty, resetTerminalState } from "@/kilocode/cli/cmd/tui/util/terminal" // kilocode_change
|
||||
import * as AppExit from "@/kilocode/tui/app-exit" // kilocode_change
|
||||
import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette"
|
||||
import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap"
|
||||
|
||||
@@ -663,14 +664,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
name: "app.exit",
|
||||
title: "Exit the app",
|
||||
slashName: "exit",
|
||||
slashAliases: ["quit", "q"],
|
||||
run: () => exit(),
|
||||
category: "System",
|
||||
},
|
||||
AppExit.command(exit), // kilocode_change
|
||||
{
|
||||
name: "app.debug",
|
||||
title: "Toggle debug panel",
|
||||
@@ -812,13 +806,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
enabled: () => {
|
||||
const ok = command.matcher.get()
|
||||
if (!ok) return false
|
||||
const current = promptRef.current
|
||||
if (!current?.focused) return true
|
||||
return current.current.input === ""
|
||||
},
|
||||
enabled: () => AppExit.enabled(command.matcher.get(), promptRef.current), // kilocode_change
|
||||
bindings: tuiConfig.keybinds.gather("app_exit", ["app.exit"]),
|
||||
}))
|
||||
|
||||
|
||||
@@ -1534,12 +1534,10 @@ export function Prompt(props: PromptProps) {
|
||||
syncExtmarksWithPromptParts()
|
||||
setCursorVersion((value) => value + 1)
|
||||
}}
|
||||
/* kilocode_change start */
|
||||
onCursorChange={() => {
|
||||
/* kilocode_change */ onCursorChange={() => {
|
||||
setCursorVersion((value) => value + 1)
|
||||
if (store.mode === "normal") auto()?.onCursorChange()
|
||||
}}
|
||||
/* kilocode_change end */
|
||||
onKeyDown={(e: { preventDefault(): void }) => {
|
||||
if (props.disabled) {
|
||||
e.preventDefault()
|
||||
@@ -1641,9 +1639,7 @@ export function Prompt(props: PromptProps) {
|
||||
</box>
|
||||
<box
|
||||
height={1}
|
||||
/* kilocode_change start */
|
||||
flexShrink={0}
|
||||
/* kilocode_change end */
|
||||
/* kilocode_change */ flexShrink={0}
|
||||
border={["left"]}
|
||||
borderColor={borderHighlight()}
|
||||
customBorderChars={{
|
||||
|
||||
@@ -164,15 +164,13 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
body={
|
||||
<Switch>
|
||||
<Match when={props.request.always.length === 1 && props.request.always[0] === "*"}>
|
||||
{/* kilocode_change start */}
|
||||
{/* kilocode_change */}
|
||||
<TextBody title={"This will allow " + props.request.permission + " permanently."} />
|
||||
{/* kilocode_change end */}
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<box paddingLeft={1} gap={1}>
|
||||
{/* kilocode_change start */}
|
||||
{/* kilocode_change */}
|
||||
<text fg={theme.textMuted}>This will allow the following patterns permanently</text>
|
||||
{/* kilocode_change end */}
|
||||
<box>
|
||||
<For each={props.request.always}>
|
||||
{(pattern) => (
|
||||
@@ -453,9 +451,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
title="Permission required"
|
||||
header={header()}
|
||||
body={current.body}
|
||||
/* kilocode_change start */
|
||||
options={options}
|
||||
/* kilocode_change end */
|
||||
/* kilocode_change */ options={options}
|
||||
escapeKey="reject"
|
||||
fullscreen
|
||||
onSelect={(option) => {
|
||||
|
||||
@@ -175,7 +175,7 @@ export const Info = Schema.Struct({
|
||||
description: "Server configuration for the kilo serve command", // kilocode_change
|
||||
}),
|
||||
command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
|
||||
description: "Command configuration, see https://opencode.ai/docs/commands", // kilocode_change
|
||||
description: "Command configuration, see https://opencode.ai/docs/commands",
|
||||
}),
|
||||
skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
|
||||
reference: Schema.optional(ConfigReference.Info).annotate({
|
||||
|
||||
@@ -3,13 +3,99 @@ import type { MessageV2 } from "@/session/message-v2"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
|
||||
const MAX_BASE64_BYTES = 4.5 * 1024 * 1024
|
||||
export const MAX_BASE64_BYTES = 4.5 * 1024 * 1024 // kilocode_change - share user file pre-read limit
|
||||
const MAX_WIDTH = 2000
|
||||
const MAX_HEIGHT = 2000
|
||||
const AUTO_RESIZE = true
|
||||
const JPEG_QUALITIES = [80, 85, 70, 55, 40]
|
||||
const log = Log.create({ service: "image" })
|
||||
|
||||
// kilocode_change start - preserve valid in-limit images when Photon is unavailable
|
||||
function dimensions(mime: string, data: Buffer) {
|
||||
if (
|
||||
mime === "image/png" &&
|
||||
data.length >= 24 &&
|
||||
data.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) &&
|
||||
data.subarray(12, 16).toString("ascii") === "IHDR"
|
||||
)
|
||||
return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) }
|
||||
|
||||
if (mime === "image/gif" && data.length >= 10) {
|
||||
const head = data.subarray(0, 6).toString("ascii")
|
||||
if (head === "GIF87a" || head === "GIF89a")
|
||||
return { width: data.readUInt16LE(6), height: data.readUInt16LE(8) }
|
||||
}
|
||||
|
||||
if ((mime === "image/jpeg" || mime === "image/jpg") && data.length >= 4 && data.readUInt16BE(0) === 0xffd8) {
|
||||
for (let offset = 2; offset + 8 < data.length; ) {
|
||||
if (data[offset] !== 0xff) {
|
||||
offset++
|
||||
continue
|
||||
}
|
||||
const marker = data[offset + 1]
|
||||
if (marker === 0xd9 || marker === 0xda) break
|
||||
const length = data.readUInt16BE(offset + 2)
|
||||
if (length < 2 || offset + length + 2 > data.length) break
|
||||
if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker))
|
||||
return { width: data.readUInt16BE(offset + 7), height: data.readUInt16BE(offset + 5) }
|
||||
offset += length + 2
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
mime === "image/webp" &&
|
||||
data.length >= 30 &&
|
||||
data.subarray(0, 4).toString("ascii") === "RIFF" &&
|
||||
data.subarray(8, 12).toString("ascii") === "WEBP"
|
||||
) {
|
||||
const chunk = data.subarray(12, 16).toString("ascii")
|
||||
if (chunk === "VP8X")
|
||||
return {
|
||||
width: 1 + data.readUIntLE(24, 3),
|
||||
height: 1 + data.readUIntLE(27, 3),
|
||||
}
|
||||
if (chunk === "VP8L" && data[20] === 0x2f)
|
||||
return {
|
||||
width: 1 + data[21] + ((data[22] & 0x3f) << 8),
|
||||
height: 1 + (data[22] >> 6) + (data[23] << 2) + ((data[24] & 0x0f) << 10),
|
||||
}
|
||||
if (chunk === "VP8 " && data[23] === 0x9d && data[24] === 0x01 && data[25] === 0x2a)
|
||||
return { width: data.readUInt16LE(26) & 0x3fff, height: data.readUInt16LE(28) & 0x3fff }
|
||||
}
|
||||
}
|
||||
|
||||
export function fallback(
|
||||
input: MessageV2.FilePart,
|
||||
base64: string,
|
||||
max: { bytes: number; width: number; height: number },
|
||||
) {
|
||||
const bytes = Buffer.byteLength(base64, "utf8")
|
||||
if (bytes > max.bytes)
|
||||
return new SizeError({
|
||||
bytes,
|
||||
max: max.bytes,
|
||||
width: 0,
|
||||
height: 0,
|
||||
max_width: max.width,
|
||||
max_height: max.height,
|
||||
})
|
||||
const data = Buffer.from(base64, "base64")
|
||||
const canonical = data.toString("base64").replace(/=+$/, "") === base64.replace(/=+$/, "")
|
||||
const size = canonical ? dimensions(input.mime, data) : undefined
|
||||
if (!base64 || !size) return new DecodeError()
|
||||
if (size.width > max.width || size.height > max.height)
|
||||
return new SizeError({
|
||||
bytes,
|
||||
max: max.bytes,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
max_width: max.width,
|
||||
max_height: max.height,
|
||||
})
|
||||
return input
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
export class PhotonUnavailableError extends Schema.TaggedErrorClass<PhotonUnavailableError>()(
|
||||
"ImagePhotonUnavailableError",
|
||||
{},
|
||||
@@ -63,11 +149,13 @@ export const layer = Layer.effect(
|
||||
try {
|
||||
const photonWasm = (await import("@silvia-odwyer/photon-node/photon_rs_bg.wasm", { with: { type: "file" } }))
|
||||
.default
|
||||
// Patched photon-node reads this during module init so Bun compiled binaries use the embedded wasm path.
|
||||
;(globalThis as typeof globalThis & { __OPENCODE_PHOTON_WASM_PATH?: string }).__OPENCODE_PHOTON_WASM_PATH =
|
||||
// kilocode_change start - use Kilo's embedded WASM path in compiled binaries
|
||||
;(globalThis as typeof globalThis & { __KILOCODE_PHOTON_WASM_PATH?: string }).__KILOCODE_PHOTON_WASM_PATH =
|
||||
photonWasm
|
||||
// kilocode_change end
|
||||
return await import("@silvia-odwyer/photon-node")
|
||||
} catch {
|
||||
} catch (err) {
|
||||
log.error("failed to load Photon image processor", { err }) // kilocode_change
|
||||
return null
|
||||
}
|
||||
}),
|
||||
@@ -86,7 +174,17 @@ export const layer = Layer.effect(
|
||||
|
||||
const base64 = input.url.slice(input.url.indexOf(";base64,") + ";base64,".length)
|
||||
const photon = yield* loadPhoton
|
||||
if (!photon) return yield* new PhotonUnavailableError()
|
||||
// kilocode_change start - fail closed on invalid bytes but preserve valid in-limit images without Photon
|
||||
if (!photon) {
|
||||
const result = fallback(input, base64, {
|
||||
bytes: info.maxBase64Bytes,
|
||||
width: info.maxWidth,
|
||||
height: info.maxHeight,
|
||||
})
|
||||
if (result instanceof Error) return yield* result
|
||||
return result
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const decoded = yield* Effect.sync(() => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export type Prompt = {
|
||||
readonly focused: boolean
|
||||
readonly current: { readonly input: string }
|
||||
}
|
||||
|
||||
export function enabled(matcher: boolean, prompt?: Prompt) {
|
||||
if (!matcher) return false
|
||||
if (!prompt?.focused) return true
|
||||
return prompt.current.input === ""
|
||||
}
|
||||
|
||||
export function command(exit: () => void) {
|
||||
return {
|
||||
name: "app.exit",
|
||||
title: "Exit the app",
|
||||
slashName: "exit",
|
||||
slashAliases: ["quit", "q"],
|
||||
run: exit,
|
||||
category: "System",
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { Image } from "@/image/image" // kilocode_change - classify user image validation defects
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { KiloSessionHttpApi } from "@/kilocode/server/httpapi/session-fork" // kilocode_change
|
||||
import { Agent } from "@/agent/agent"
|
||||
@@ -269,7 +270,18 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
.pipe(
|
||||
Effect.provideService(InstanceRef, instance),
|
||||
Effect.provideService(WorkspaceRef, workspace),
|
||||
Effect.mapError(() => new HttpApiError.BadRequest({})),
|
||||
// kilocode_change start - reject only typed user image validation defects as request errors
|
||||
Effect.catchCause((cause) => {
|
||||
const error = Cause.squash(cause)
|
||||
if (
|
||||
error instanceof Image.InvalidDataUrlError ||
|
||||
error instanceof Image.DecodeError ||
|
||||
error instanceof Image.SizeError
|
||||
)
|
||||
return Effect.fail(new HttpApiError.BadRequest({}))
|
||||
return Effect.failCause(cause)
|
||||
}),
|
||||
// kilocode_change end
|
||||
)
|
||||
return HttpServerResponse.stream(Stream.make(JSON.stringify(message)).pipe(Stream.encodeText), {
|
||||
contentType: "application/json",
|
||||
|
||||
@@ -638,6 +638,7 @@ export const layer: Layer.Layer<
|
||||
include: selected.tail_start_id,
|
||||
})
|
||||
}
|
||||
// kilocode_change start - export self-contained compaction capture
|
||||
const parent = KiloSession.resolveParent(input.sessionID)
|
||||
const found = KiloSession.resolveRoot(input.sessionID)
|
||||
const root = parent ? (found === input.sessionID ? parent : found) : input.sessionID
|
||||
@@ -666,11 +667,12 @@ export const layer: Layer.Layer<
|
||||
outputTokens: processor.message.tokens.output,
|
||||
},
|
||||
})
|
||||
// kilocode_change end
|
||||
yield* prune({ sessionID: input.sessionID, reason: "post-compaction" })
|
||||
yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
|
||||
}
|
||||
return fallback
|
||||
// kilocode_change end
|
||||
return fallback // kilocode_change
|
||||
})
|
||||
|
||||
const create = Effect.fn("SessionCompaction.create")(function* (input: {
|
||||
|
||||
@@ -69,6 +69,7 @@ import * as DateTime from "effect/DateTime"
|
||||
import { eq } from "@/storage/db"
|
||||
import * as Database from "@/storage/db"
|
||||
import { SessionTable } from "./session.sql"
|
||||
import { Image } from "@/image/image" // kilocode_change - normalize user image data before persistence
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
@@ -132,6 +133,7 @@ export const layer = Layer.effect(
|
||||
const summary = yield* SessionSummary.Service
|
||||
const sys = yield* SystemPrompt.Service
|
||||
const llm = yield* LLM.Service
|
||||
const image = yield* Image.Service // kilocode_change - normalize user image data before persistence
|
||||
const sync = yield* SyncEvent.Service // kilocode_change - preserve Kilo v2 event dual-write wiring
|
||||
const runner = Effect.fn("SessionPrompt.runner")(function* () {
|
||||
return yield* EffectBridge.make()
|
||||
@@ -842,7 +844,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
providerID: model.providerID,
|
||||
}
|
||||
yield* sessions.updateMessage(msg)
|
||||
const callID = ulid()
|
||||
const callID = ulid() // kilocode_change - correlate v2 shell events with the persisted tool part
|
||||
const started = Date.now()
|
||||
const part: MessageV2.ToolPart = {
|
||||
type: "tool",
|
||||
@@ -850,7 +852,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
messageID: msg.id,
|
||||
sessionID: input.sessionID,
|
||||
tool: ShellID.ToolID,
|
||||
callID: ulid(),
|
||||
callID, // kilocode_change
|
||||
state: {
|
||||
status: "running",
|
||||
time: { start: started },
|
||||
@@ -1143,6 +1145,17 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
{ ...part, messageID: info.id, sessionID: input.sessionID },
|
||||
]
|
||||
}
|
||||
// kilocode_change start - normalize user image data before persistence
|
||||
if (part.mime.startsWith("image/")) {
|
||||
const file: MessageV2.FilePart = {
|
||||
...part,
|
||||
id: part.id ? PartID.make(part.id) : PartID.ascending(),
|
||||
messageID: info.id,
|
||||
sessionID: input.sessionID,
|
||||
}
|
||||
return [yield* image.normalize(file).pipe(Effect.orDie)]
|
||||
}
|
||||
// kilocode_change end
|
||||
break
|
||||
case "file:": {
|
||||
log.info("file", { mime: part.mime })
|
||||
@@ -1285,6 +1298,39 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
]
|
||||
}
|
||||
|
||||
// kilocode_change start - reject oversized user image files before reading and base64 allocation
|
||||
if (mime.startsWith("image/")) {
|
||||
const limit = (yield* config.get()).attachment?.image?.max_base64_bytes ?? Image.MAX_BASE64_BYTES
|
||||
const stat = yield* fsys.stat(filepath).pipe(Effect.catch(Effect.die))
|
||||
const encoded = ((stat.size + 2n) / 3n) * 4n
|
||||
if (encoded > BigInt(limit))
|
||||
return yield* Effect.die(
|
||||
new Image.SizeError({
|
||||
bytes: Number(encoded > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : encoded),
|
||||
max: limit,
|
||||
width: 0,
|
||||
height: 0,
|
||||
max_width: 0,
|
||||
max_height: 0,
|
||||
}),
|
||||
)
|
||||
}
|
||||
// kilocode_change end
|
||||
const file: MessageV2.FilePart = {
|
||||
id: part.id ? PartID.make(part.id) : PartID.ascending(),
|
||||
messageID: info.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "file",
|
||||
url:
|
||||
`data:${mime};base64,` +
|
||||
Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"),
|
||||
mime,
|
||||
filename: part.filename!,
|
||||
source: part.source,
|
||||
}
|
||||
// kilocode_change start - apply image limits after resolving user file URLs
|
||||
const attachment = mime.startsWith("image/") ? yield* image.normalize(file).pipe(Effect.orDie) : file
|
||||
// kilocode_change end
|
||||
return [
|
||||
{
|
||||
messageID: info.id,
|
||||
@@ -1293,18 +1339,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
synthetic: true,
|
||||
text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`,
|
||||
},
|
||||
{
|
||||
id: part.id,
|
||||
messageID: info.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "file",
|
||||
url:
|
||||
`data:${mime};base64,` +
|
||||
Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"),
|
||||
mime,
|
||||
filename: part.filename!,
|
||||
source: part.source,
|
||||
},
|
||||
attachment,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2062,6 +2097,8 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
Layer.provide(LSP.defaultLayer),
|
||||
Layer.provide(ToolRegistry.defaultLayer),
|
||||
Layer.provide(Truncate.defaultLayer),
|
||||
).pipe(
|
||||
Layer.provide(Image.defaultLayer), // kilocode_change - provide user image normalization service
|
||||
Layer.provide(Provider.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// kilocode_change - new file
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { TuiConfig } from "../../../../src/cli/cmd/tui/config/tui"
|
||||
import * as AppExit from "../../../../src/kilocode/tui/app-exit"
|
||||
|
||||
const prompt = (focused: boolean, input: string): AppExit.Prompt => ({
|
||||
focused,
|
||||
current: { input },
|
||||
})
|
||||
|
||||
describe("app_exit", () => {
|
||||
test("blocks every configured exit binding when the command matcher is disabled", () => {
|
||||
const bindings = TuiConfig.resolve({}).keybinds.gather("app_exit", ["app.exit"])
|
||||
|
||||
expect(bindings.length).toBeGreaterThan(0)
|
||||
for (const binding of bindings) expect(AppExit.enabled(false)).toBe(false)
|
||||
})
|
||||
|
||||
test("permits exit without a prompt ref", () => {
|
||||
expect(AppExit.enabled(true)).toBe(true)
|
||||
})
|
||||
|
||||
test("blocks focused prompts with non-empty input including whitespace", () => {
|
||||
expect(AppExit.enabled(true, prompt(true, "keep typing"))).toBe(false)
|
||||
expect(AppExit.enabled(true, prompt(true, " "))).toBe(false)
|
||||
})
|
||||
|
||||
test("permits focused empty and unfocused prompts", () => {
|
||||
expect(AppExit.enabled(true, prompt(true, ""))).toBe(true)
|
||||
expect(AppExit.enabled(true, prompt(false, "keep typing"))).toBe(true)
|
||||
})
|
||||
|
||||
test("registers slash exit independently of binding enablement", () => {
|
||||
let exited = false
|
||||
const command = AppExit.command(() => {
|
||||
exited = true
|
||||
})
|
||||
|
||||
expect(command).toMatchObject({
|
||||
name: "app.exit",
|
||||
slashName: "exit",
|
||||
slashAliases: ["quit", "q"],
|
||||
})
|
||||
command.run()
|
||||
expect(exited).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import { Image } from "@/image/image"
|
||||
import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||
@@ -57,6 +57,46 @@ describe("Image", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
// kilocode_change start - cover Kilo's Photon-unavailable fallback
|
||||
test("preserves a valid in-limit image without Photon", () => {
|
||||
const data = "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA"
|
||||
const input = part("image/webp", data)
|
||||
|
||||
expect(Image.fallback(input, data, { bytes: 1024, width: 2000, height: 2000 })).toEqual(input)
|
||||
})
|
||||
|
||||
test("rejects non-image bytes without Photon", () => {
|
||||
const data = Buffer.from("not an image").toString("base64")
|
||||
const result = Image.fallback(part("image/png", data), data, { bytes: 1024, width: 2000, height: 2000 })
|
||||
|
||||
expect(result).toBeInstanceOf(Image.DecodeError)
|
||||
})
|
||||
|
||||
test("rejects oversized encoded input before decoding without Photon", () => {
|
||||
const data = "A".repeat(8 * 1024 * 1024)
|
||||
const result = Image.fallback(part("image/png", data), data, { bytes: 1024, width: 2000, height: 2000 })
|
||||
|
||||
expect(result).toBeInstanceOf(Image.SizeError)
|
||||
if (result instanceof Image.SizeError) expect(result.bytes).toBe(data.length)
|
||||
})
|
||||
|
||||
test("rejects an image with oversized header dimensions without Photon", () => {
|
||||
const png = Buffer.alloc(24)
|
||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).copy(png)
|
||||
png.write("IHDR", 12, "ascii")
|
||||
png.writeUInt32BE(10_000, 16)
|
||||
png.writeUInt32BE(1, 20)
|
||||
const data = png.toString("base64")
|
||||
const result = Image.fallback(part("image/png", data), data, { bytes: 1024, width: 2000, height: 2000 })
|
||||
|
||||
expect(result).toBeInstanceOf(Image.SizeError)
|
||||
if (result instanceof Image.SizeError) {
|
||||
expect(result.width).toBe(10_000)
|
||||
expect(result.height).toBe(1)
|
||||
}
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
tiny.effect("fails with a typed size error when no resized candidate fits", () =>
|
||||
Effect.gen(function* () {
|
||||
const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node"))
|
||||
|
||||
@@ -168,6 +168,7 @@ function makeHttp() {
|
||||
TestLLMServer.layer,
|
||||
SessionPrompt.layer.pipe(
|
||||
Layer.provide(SessionRevert.defaultLayer),
|
||||
Layer.provide(Image.defaultLayer),
|
||||
Layer.provide(summary),
|
||||
Layer.provideMerge(runState),
|
||||
Layer.provideMerge(compact),
|
||||
|
||||
@@ -26,10 +26,9 @@ describe("session export worker e2e", () => {
|
||||
worker.postMessage({ kind: "event", approxBytes: 1000, envelope: env })
|
||||
}
|
||||
await until(() => rows(db).length === 3)
|
||||
const out = rows(db)
|
||||
await shutdown(worker)
|
||||
worker.terminate()
|
||||
|
||||
const out = rows(db)
|
||||
expect(out.map((row) => row.type)).toEqual([
|
||||
"llm_request_started",
|
||||
"workspace_baseline_completed",
|
||||
|
||||
@@ -161,6 +161,7 @@ function makeHttp() {
|
||||
TestLLMServer.layer,
|
||||
SessionPrompt.layer.pipe(
|
||||
Layer.provide(SessionRevert.defaultLayer),
|
||||
Layer.provide(Image.defaultLayer),
|
||||
Layer.provide(summary),
|
||||
Layer.provideMerge(run),
|
||||
Layer.provideMerge(compact),
|
||||
|
||||
@@ -155,6 +155,7 @@ function makeHttp() {
|
||||
TestLLMServer.layer,
|
||||
SessionPrompt.layer.pipe(
|
||||
Layer.provide(SessionRevert.defaultLayer),
|
||||
Layer.provide(Image.defaultLayer),
|
||||
Layer.provide(summary),
|
||||
Layer.provideMerge(run),
|
||||
Layer.provideMerge(compact),
|
||||
|
||||
@@ -654,6 +654,75 @@ describe("HttpApi SDK", () => {
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start - verify invalid user images fail at the real SDK boundary
|
||||
serverPathParity("rejects malformed user image data before persistence", (serverPath) =>
|
||||
withStandardProject(serverPath, ({ sdk }) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* capture(() => sdk.session.create({ title: "invalid image" }))
|
||||
const sessionID = String(record(session.data).id)
|
||||
const prompt = yield* capture(() =>
|
||||
sdk.session.prompt({
|
||||
sessionID,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "not-an-image.png",
|
||||
url: "data:image/png;base64,bm90LWltYWdl",
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
const messages = yield* capture(() => sdk.session.messages({ sessionID }))
|
||||
|
||||
expect(prompt.status).toBe(400)
|
||||
expect(JSON.stringify(messages.data)).not.toContain("not-an-image.png")
|
||||
|
||||
return {
|
||||
promptStatus: prompt.status,
|
||||
persisted: JSON.stringify(messages.data).includes("not-an-image.png"),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
serverPathParity("rejects oversized user image files before persistence", (serverPath) =>
|
||||
withProject(
|
||||
serverPath,
|
||||
{ config: { attachment: { image: { max_base64_bytes: 4 } } } },
|
||||
({ sdk, directory }) =>
|
||||
Effect.gen(function* () {
|
||||
const filepath = path.join(directory, "oversized.png")
|
||||
yield* call(() => Bun.write(filepath, Buffer.alloc(1024, 1)))
|
||||
const session = yield* capture(() => sdk.session.create({ title: "oversized image" }))
|
||||
const sessionID = String(record(session.data).id)
|
||||
const prompt = yield* capture(() =>
|
||||
sdk.session.prompt({
|
||||
sessionID,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "oversized.png",
|
||||
url: `file://${filepath}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
const messages = yield* capture(() => sdk.session.messages({ sessionID }))
|
||||
|
||||
expect(prompt.status).toBe(400)
|
||||
expect(JSON.stringify(messages.data)).not.toContain("oversized.png")
|
||||
|
||||
return { promptStatus: prompt.status, persisted: JSON.stringify(messages.data).includes("oversized.png") }
|
||||
}),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
serverPathParity("matches generated SDK prompt streaming through fake LLM", (serverPath) =>
|
||||
withFakeLlm(serverPath, ({ sdk, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -688,6 +757,62 @@ describe("HttpApi SDK", () => {
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start - verify provider errors remain in successful assistant messages
|
||||
serverPathParity("preserves provider errors through the generated SDK", (serverPath) =>
|
||||
withFakeLlm(serverPath, ({ sdk, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const gateway = { error: { code: "PAID_MODEL_AUTH_REQUIRED", message: "Authentication required" } }
|
||||
const create = () =>
|
||||
capture(() =>
|
||||
sdk.session.create({
|
||||
title: "provider error",
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
}),
|
||||
)
|
||||
const prompt = (sessionID: string) => ({
|
||||
sessionID,
|
||||
agent: "build",
|
||||
model: { providerID: "test", modelID: "test-model" },
|
||||
parts: [{ type: "text" as const, text: "trigger provider error" }],
|
||||
})
|
||||
|
||||
yield* llm.error(401, gateway)
|
||||
const tupleSession = yield* create()
|
||||
const tuple = yield* capture(() => sdk.session.prompt(prompt(String(record(tupleSession.data).id))))
|
||||
|
||||
yield* llm.error(401, gateway)
|
||||
const strictSession = yield* create()
|
||||
const strict = yield* call(() =>
|
||||
sdk.session.prompt(prompt(String(record(strictSession.data).id)), { throwOnError: true }),
|
||||
)
|
||||
|
||||
const tupleError = record(record(tuple.data).info).error
|
||||
const tupleData = record(record(tupleError).data)
|
||||
const strictError = record(record(record(strict).data).info).error
|
||||
const strictData = record(record(strictError).data)
|
||||
|
||||
expect(tuple.status).toBe(200)
|
||||
expect(record(tupleError).name).toBe("APIError")
|
||||
expect(tupleData.statusCode).toBe(401)
|
||||
expect(JSON.parse(String(tupleData.responseBody))).toEqual(gateway)
|
||||
expect(record(strictError).name).toBe("APIError")
|
||||
expect(strictData.statusCode).toBe(401)
|
||||
expect(JSON.parse(String(strictData.responseBody))).toEqual(gateway)
|
||||
|
||||
return {
|
||||
tupleStatus: tuple.status,
|
||||
tupleName: record(tupleError).name,
|
||||
providerStatus: tupleData.statusCode,
|
||||
providerBody: tupleData.responseBody,
|
||||
strictName: record(strictError).name,
|
||||
strictStatus: strictData.statusCode,
|
||||
strictBody: strictData.responseBody,
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
httpapi(
|
||||
"includes project skills in REST API async prompt context",
|
||||
withFakeLlmProject("default", { setup: writeProjectSkill }, ({ sdk, llm }) =>
|
||||
|
||||
@@ -412,7 +412,7 @@ describe("session.llm.stream", () => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json", // kilocode_change
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: [providerID],
|
||||
provider: {
|
||||
[providerID]: {
|
||||
@@ -502,7 +502,7 @@ describe("session.llm.stream", () => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json", // kilocode_change
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: [providerID],
|
||||
provider: {
|
||||
[providerID]: {
|
||||
@@ -725,7 +725,7 @@ describe("session.llm.stream", () => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json", // kilocode_change
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: ["openai"],
|
||||
provider: {
|
||||
openai: {
|
||||
@@ -962,7 +962,7 @@ describe("session.llm.stream", () => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json", // kilocode_change
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
enabled_providers: ["anthropic"],
|
||||
provider: {
|
||||
anthropic: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Telemetry } from "@kilocode/kilo-telemetry"
|
||||
// kilocode_change end
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||
import { Bus } from "../../src/bus"
|
||||
@@ -454,6 +454,113 @@ it.live("new prompt dismisses a pending question", () =>
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - cover user image normalization before persistence
|
||||
it.live("normalizes user data images before persistence", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const chat = yield* sessions.create({ title: "User image" })
|
||||
const url = "data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA"
|
||||
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "file", mime: "image/webp", filename: "pixel.webp", url }],
|
||||
})
|
||||
|
||||
expect(result.parts).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ type: "file", mime: "image/webp", url })]),
|
||||
)
|
||||
const saved = yield* sessions.messages({ sessionID: chat.id })
|
||||
expect(saved.flatMap((message) => message.parts)).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ type: "file", mime: "image/webp", url })]),
|
||||
)
|
||||
}),
|
||||
{ git: true, config: providerCfg },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("rejects malformed user data images before persistence", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const chat = yield* sessions.create({ title: "Invalid user image" })
|
||||
const exit = yield* prompt
|
||||
.prompt({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "file",
|
||||
mime: "image/png",
|
||||
filename: "invalid.png",
|
||||
url: `data:image/png;base64,${Buffer.from("not an image").toString("base64")}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
const saved = yield* sessions.messages({ sessionID: chat.id })
|
||||
expect(saved.flatMap((message) => message.parts).some((part) => part.type === "file")).toBe(false)
|
||||
}),
|
||||
{ git: true, config: providerCfg },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("normalizes user image file URLs after reading them", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* ({ dir }) {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const chat = yield* sessions.create({ title: "User image file" })
|
||||
const data = "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA"
|
||||
const filepath = path.join(dir, "pixel.webp")
|
||||
yield* Effect.promise(() => Bun.write(filepath, Buffer.from(data, "base64")))
|
||||
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "file", mime: "image/webp", filename: "pixel.webp", url: pathToFileURL(filepath).href }],
|
||||
})
|
||||
|
||||
expect(result.parts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: "file", mime: "image/webp", url: `data:image/webp;base64,${data}` }),
|
||||
]),
|
||||
)
|
||||
}),
|
||||
{ git: true, config: providerCfg },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("leaves non-image data parts untouched", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const chat = yield* sessions.create({ title: "User data" })
|
||||
const url = "data:application/octet-stream;base64,bm90IGFuIGltYWdl"
|
||||
|
||||
const result = yield* prompt.prompt({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [{ type: "file", mime: "application/octet-stream", filename: "data.bin", url }],
|
||||
})
|
||||
|
||||
expect(result.parts).toEqual(expect.arrayContaining([expect.objectContaining({ type: "file", url })]))
|
||||
}),
|
||||
{ git: true, config: providerCfg },
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
it.live("prompt emits v2 prompted and synthetic events", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* () {
|
||||
@@ -1423,6 +1530,38 @@ unix("shell commands can change directory after startup", () =>
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start - verify shell v2 events correlate with the persisted tool part
|
||||
unix("shell correlates the persisted tool part with its completed v2 record", () =>
|
||||
provideTmpdirInstance(
|
||||
(_dir) =>
|
||||
Effect.gen(function* () {
|
||||
const { prompt, chat } = yield* boot()
|
||||
const result = yield* prompt.shell({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
command: "printf correlated",
|
||||
})
|
||||
const tool = completedTool(result.parts)
|
||||
if (!tool) return
|
||||
|
||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||
Effect.provide(SessionV2.layer),
|
||||
)
|
||||
const shell = messages.find((message) => message.type === "shell")
|
||||
|
||||
expect(shell).toMatchObject({
|
||||
type: "shell",
|
||||
callID: tool.callID,
|
||||
command: "printf correlated",
|
||||
output: "correlated",
|
||||
time: { completed: expect.anything() },
|
||||
})
|
||||
}),
|
||||
{ git: true, config: cfg },
|
||||
),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
unix("shell lists files from the project directory", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
@@ -1912,7 +2051,7 @@ it.live(
|
||||
),
|
||||
]),
|
||||
)
|
||||
}),
|
||||
}).pipe(Effect.scoped), // kilocode_change - scope test finalizers explicitly
|
||||
{ git: true, config: cfg },
|
||||
),
|
||||
30_000,
|
||||
@@ -1955,7 +2094,7 @@ it.live(
|
||||
),
|
||||
]),
|
||||
)
|
||||
}),
|
||||
}).pipe(Effect.scoped), // kilocode_change - scope test finalizers explicitly
|
||||
{ git: true, config: cfg },
|
||||
),
|
||||
30_000,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// kilocode_change - new file
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { EventV2 } from "../../src/v2/event"
|
||||
import { SessionEvent } from "../../src/v2/session-event"
|
||||
import { SessionMessageUpdater } from "../../src/v2/session-message-updater"
|
||||
|
||||
describe("v2 shell event correlation", () => {
|
||||
test("an unmatched end is ignored before a matching start and end complete one record", () => {
|
||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
const callID = "call"
|
||||
const updater = SessionMessageUpdater.memory(state)
|
||||
|
||||
SessionMessageUpdater.update(updater, {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.shell.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(0),
|
||||
callID: "missing",
|
||||
output: "ignored",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
expect(state.messages).toEqual([])
|
||||
|
||||
SessionMessageUpdater.update(updater, {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.shell.started",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
callID,
|
||||
command: "pwd",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(updater, {
|
||||
id: EventV2.ID.create(),
|
||||
type: "session.next.shell.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
callID,
|
||||
output: "/tmp",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
expect(state.messages).toHaveLength(1)
|
||||
expect(state.messages[0]).toMatchObject({
|
||||
type: "shell",
|
||||
callID,
|
||||
command: "pwd",
|
||||
output: "/tmp",
|
||||
time: {
|
||||
created: DateTime.makeUnsafe(1),
|
||||
completed: DateTime.makeUnsafe(2),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -30,11 +30,11 @@ const node = CrossSpawnSpawner.defaultLayer
|
||||
|
||||
const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
|
||||
|
||||
const unix = process.platform !== "win32" ? it.live : it.live.skip // kilocode_change
|
||||
// kilocode_change - skip on windows: address windows ci failures #9496
|
||||
const unix = process.platform !== "win32" ? it.live : it.live.skip
|
||||
|
||||
describe("tool.skill", () => {
|
||||
unix("execute returns skill content block with files", () =>
|
||||
// kilocode_change
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -7,8 +7,8 @@ index 8f4144d..b83e9a9 100644
|
||||
};
|
||||
|
||||
-const path = require('path').join(__dirname, 'photon_rs_bg.wasm');
|
||||
+// Allow opencode's Bun compiled binary to point photon-node at its embedded wasm asset.
|
||||
+const path = globalThis.__OPENCODE_PHOTON_WASM_PATH || require('path').join(__dirname, 'photon_rs_bg.wasm');
|
||||
+// Allow Kilo's Bun compiled binary to point photon-node at its embedded wasm asset.
|
||||
+const path = globalThis.__KILOCODE_PHOTON_WASM_PATH || require('path').join(__dirname, 'photon_rs_bg.wasm');
|
||||
const bytes = require('fs').readFileSync(path);
|
||||
|
||||
const wasmModule = new WebAssembly.Module(bytes);
|
||||
|
||||
Reference in New Issue
Block a user