mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
fix(cli): bound XLSX read input size
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Read Jupyter notebooks as ordered markdown and code cell content instead of raw notebook payloads.
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"packages/kilo-docs": {
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@docsearch/css": "^4",
|
||||
"@docsearch/js": "^4",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/kilo-gateway": {
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@ai-sdk/alibaba": "1.0.17",
|
||||
"@ai-sdk/anthropic": "3.0.71",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/kilo-i18n": {
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/kilo-indexing": {
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
|
||||
"@aws-sdk/credential-provider-ini": "3.972.31",
|
||||
@@ -176,11 +176,11 @@
|
||||
},
|
||||
"packages/kilo-jetbrains": {
|
||||
"name": "@kilocode/kilo-jetbrains",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
},
|
||||
"packages/kilo-telemetry": {
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
"posthog-node": "4.4.0",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/kilo-ui": {
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -231,7 +231,7 @@
|
||||
},
|
||||
"packages/kilo-vscode": {
|
||||
"name": "kilo-code",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@kilocode/kilo-gateway": "workspace:*",
|
||||
@@ -296,7 +296,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "@kilocode/cli",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"bin": {
|
||||
"kilo": "./bin/kilo",
|
||||
"kilocode": "./bin/kilo",
|
||||
@@ -455,7 +455,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -480,7 +480,7 @@
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"semver": "^7.6.3",
|
||||
},
|
||||
@@ -491,7 +491,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -506,7 +506,7 @@
|
||||
},
|
||||
"packages/storybook": {
|
||||
"name": "@opencode-ai/storybook",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"devDependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@solidjs/meta": "catalog:",
|
||||
@@ -529,7 +529,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@kobalte/core": "catalog:",
|
||||
|
||||
+1
-1
@@ -149,6 +149,6 @@
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
},
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "kilo"
|
||||
name = "Kilo"
|
||||
description = "The open source coding agent."
|
||||
version = "7.3.16"
|
||||
version = "7.3.17"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/Kilo-Org/kilocode"
|
||||
@@ -11,26 +11,26 @@ name = "Kilo"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.16/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.16/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.16/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.16/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.16/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.17/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-docs",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 3002",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-gateway",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-i18n",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Kilo-specific i18n translations and overrides",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-indexing",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Standalone indexing engine and host helpers for Kilo Code",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"test": "./gradlew test",
|
||||
"test:ci": "bun script/test-ci.ts"
|
||||
},
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/kilo-telemetry",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "kilo-code",
|
||||
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
|
||||
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"icon": "assets/icons/logo-outline-black.png",
|
||||
"galleryBanner": {
|
||||
"color": "#FFFFFF",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"peerDependencies": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"name": "@kilocode/cli",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -214,6 +214,7 @@ export namespace KiloSessions {
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const config = yield* Config.Service
|
||||
const sessions = yield* Session.Service
|
||||
const state = yield* InstanceState.make(
|
||||
Effect.fn("KiloSessions.state")(function* () {
|
||||
if (ingestDisabled) return
|
||||
@@ -239,7 +240,7 @@ export namespace KiloSessions {
|
||||
})
|
||||
yield* watch(Session.Event.Updated, async (evt) => {
|
||||
const sessionID = evt.properties.sessionID
|
||||
const session = await Session.get(sessionID).catch(() => null)
|
||||
const session = await Effect.runPromise(sessions.get(sessionID).pipe(Effect.orElseSucceed(() => null)))
|
||||
if (!session) return
|
||||
await ingest.sync(sessionID, [
|
||||
{ type: "kilo_meta", data: await meta(sessionID) },
|
||||
@@ -326,7 +327,11 @@ export namespace KiloSessions {
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Bus.layer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
)
|
||||
|
||||
export async function enableRemote() {
|
||||
if (remote) return
|
||||
@@ -364,19 +369,24 @@ export namespace KiloSessions {
|
||||
const ids = new Set(Object.keys(statuses))
|
||||
for (const id of focused) ids.add(id)
|
||||
for (const id of opened) ids.add(id)
|
||||
const results = await Promise.all(
|
||||
[...ids].map(async (id) => {
|
||||
const session = await Session.get(SessionID.make(id)).catch(() => undefined)
|
||||
if (!session) return undefined
|
||||
return {
|
||||
id,
|
||||
status: statuses[id]?.type ?? "idle",
|
||||
title: session.title,
|
||||
parentSessionId: session.parentID,
|
||||
gitUrl,
|
||||
gitBranch,
|
||||
}
|
||||
}),
|
||||
const results = await AppRuntime.runPromise(
|
||||
Session.Service.use((svc) =>
|
||||
Effect.all(
|
||||
[...ids].map((id) =>
|
||||
svc.get(SessionID.make(id)).pipe(
|
||||
Effect.map((session) => ({
|
||||
id,
|
||||
status: statuses[id]?.type ?? "idle" as const,
|
||||
title: session.title,
|
||||
parentSessionId: session.parentID,
|
||||
gitUrl,
|
||||
gitBranch,
|
||||
})),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
const sessions = results.filter((r): r is NonNullable<typeof r> => !!r)
|
||||
return {
|
||||
@@ -646,10 +656,16 @@ export namespace KiloSessions {
|
||||
async function fullSync(sessionId: string) {
|
||||
log.info("full sync", { sessionId })
|
||||
|
||||
const session = await Session.get(SessionID.make(sessionId))
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
const diffs = await AppRuntime.runPromise(
|
||||
SessionSummary.Service.use((svc) => svc.diff({ sessionID: SessionID.make(sessionId) })),
|
||||
const [session, diffs] = await AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const summary = yield* SessionSummary.Service
|
||||
return yield* Effect.all([
|
||||
sessions.get(SessionID.make(sessionId)),
|
||||
summary.diff({ sessionID: SessionID.make(sessionId) }),
|
||||
])
|
||||
}),
|
||||
)
|
||||
const messages = await Array.fromAsync(MessageV2.stream(SessionID.make(sessionId)))
|
||||
messages.reverse()
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import z from "zod"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import { Effect } from "effect"
|
||||
|
||||
type Provide = typeof import("@/project/with-instance").provide
|
||||
|
||||
@@ -116,7 +117,10 @@ export namespace RemoteSender {
|
||||
})
|
||||
|
||||
async function directoryFor(sid: string): Promise<string> {
|
||||
const info = await Session.get(SessionID.make(sid)).catch(() => undefined)
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
const info = await AppRuntime.runPromise(
|
||||
Session.Service.use((svc) => svc.get(SessionID.make(sid)).pipe(Effect.orElseSucceed(() => undefined))),
|
||||
)
|
||||
return info?.directory ?? options.directory
|
||||
}
|
||||
|
||||
@@ -199,7 +203,10 @@ export namespace RemoteSender {
|
||||
}
|
||||
|
||||
async function discoverChildren(parentId: string) {
|
||||
const childSessions = await Session.children(SessionID.make(parentId))
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
const childSessions = await AppRuntime.runPromise(
|
||||
Session.Service.use((svc) => svc.children(SessionID.make(parentId))),
|
||||
)
|
||||
for (const child of childSessions) {
|
||||
children.set(child.id, parentId)
|
||||
const root = rootOf(child.id) ?? parentId
|
||||
|
||||
@@ -13,6 +13,7 @@ export namespace AllowEverythingPermission {
|
||||
export function effect(input: Input) {
|
||||
return Effect.gen(function* () {
|
||||
const svc = yield* Permission.Service
|
||||
const sessions = yield* Session.Service
|
||||
const cfg = yield* Config.Service
|
||||
const bus = yield* Bus.Service
|
||||
const rules: Permission.Ruleset = [{ permission: "*", pattern: "*", action: "allow" }]
|
||||
@@ -20,15 +21,13 @@ export namespace AllowEverythingPermission {
|
||||
if (!input.enable) {
|
||||
if (input.sessionID) {
|
||||
const id = SessionID.make(input.sessionID)
|
||||
const session = yield* Effect.promise(() => Session.get(id))
|
||||
yield* Effect.promise(() =>
|
||||
Session.setPermission({
|
||||
sessionID: id,
|
||||
permission: (session.permission ?? []).filter(
|
||||
(rule) => !(rule.permission === "*" && rule.pattern === "*" && rule.action === "allow"),
|
||||
),
|
||||
}),
|
||||
)
|
||||
const session = yield* sessions.get(id).pipe(Effect.orDie)
|
||||
yield* sessions.setPermission({
|
||||
sessionID: id,
|
||||
permission: (session.permission ?? []).filter(
|
||||
(rule) => !(rule.permission === "*" && rule.pattern === "*" && rule.action === "allow"),
|
||||
),
|
||||
})
|
||||
yield* svc.allowEverything({ enable: false, sessionID: id })
|
||||
return true
|
||||
}
|
||||
@@ -41,13 +40,11 @@ export namespace AllowEverythingPermission {
|
||||
|
||||
if (input.sessionID) {
|
||||
const id = SessionID.make(input.sessionID)
|
||||
const session = yield* Effect.promise(() => Session.get(id))
|
||||
yield* Effect.promise(() =>
|
||||
Session.setPermission({
|
||||
sessionID: id,
|
||||
permission: [...(session.permission ?? []), ...rules],
|
||||
}),
|
||||
)
|
||||
const session = yield* sessions.get(id).pipe(Effect.orDie)
|
||||
yield* sessions.setPermission({
|
||||
sessionID: id,
|
||||
permission: [...(session.permission ?? []), ...rules],
|
||||
})
|
||||
}
|
||||
|
||||
if (!input.sessionID) {
|
||||
|
||||
@@ -60,6 +60,10 @@ export const PlanFollowupRuntime = {
|
||||
handover(input: LLM.StreamInput, signal: AbortSignal) {
|
||||
return llm().runPromise((svc) => KiloLLM.text(svc.stream(input)).pipe(Effect.orDie), { signal })
|
||||
},
|
||||
async session<A, E>(run: (svc: Session.Interface) => Effect.Effect<A, E>) {
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
return AppRuntime.runPromise(Session.Service.use(run))
|
||||
},
|
||||
async loop(sessionID: SessionID) {
|
||||
const item = await import("@/session/prompt")
|
||||
const prompt = makeRuntime(item.SessionPrompt.Service, item.SessionPrompt.defaultLayer)
|
||||
@@ -242,7 +246,7 @@ export namespace PlanFollowup {
|
||||
if (text) return text
|
||||
|
||||
// Fall back to plan file on disk
|
||||
const session = await Session.get(SessionID.make(input.sessionID))
|
||||
const session = await PlanFollowupRuntime.session((svc) => svc.get(SessionID.make(input.sessionID)))
|
||||
const file = Bun.file(Session.plan(session, Instance.current))
|
||||
const plan = await file.text().catch(() => "")
|
||||
return plan.trim()
|
||||
@@ -265,15 +269,19 @@ export namespace PlanFollowup {
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
}
|
||||
await Session.updateMessage(msg)
|
||||
await Session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: msg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text: input.text,
|
||||
synthetic: input.synthetic ?? true,
|
||||
} satisfies MessageV2.TextPart)
|
||||
await PlanFollowupRuntime.session((svc) =>
|
||||
Effect.gen(function* () {
|
||||
yield* svc.updateMessage(msg)
|
||||
yield* svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: msg.id,
|
||||
sessionID: input.sessionID,
|
||||
type: "text",
|
||||
text: input.text,
|
||||
synthetic: input.synthetic ?? true,
|
||||
} satisfies MessageV2.TextPart)
|
||||
}),
|
||||
)
|
||||
return msg
|
||||
}
|
||||
|
||||
@@ -338,7 +346,7 @@ export namespace PlanFollowup {
|
||||
const code = await resolveCodeModel({
|
||||
model: input.model,
|
||||
})
|
||||
const session = await Session.get(input.sessionID)
|
||||
const session = await PlanFollowupRuntime.session((svc) => svc.get(input.sessionID))
|
||||
const { WithInstance } = await import("@/project/with-instance")
|
||||
|
||||
await WithInstance.provide({
|
||||
@@ -348,7 +356,7 @@ export namespace PlanFollowup {
|
||||
// VS Code extension's pendingFollowup gate (30s TTL) is still fresh. The
|
||||
// handover generation below can take tens of seconds and must not block
|
||||
// the SSE event that drives the webview tab switch.
|
||||
const next = await Session.create({})
|
||||
const next = await PlanFollowupRuntime.session((svc) => svc.create({}))
|
||||
const ctl = new AbortController()
|
||||
pending.set(next.id, ctl)
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
@@ -389,16 +397,20 @@ export namespace PlanFollowup {
|
||||
agent: "code",
|
||||
model: code.model,
|
||||
}
|
||||
await Session.updateMessage(msg)
|
||||
const pid = PartID.ascending()
|
||||
await Session.updatePart({
|
||||
id: pid,
|
||||
messageID: msg.id,
|
||||
sessionID: next.id,
|
||||
type: "text",
|
||||
text: compose(""),
|
||||
synthetic: false,
|
||||
} satisfies MessageV2.TextPart)
|
||||
await PlanFollowupRuntime.session((svc) =>
|
||||
Effect.gen(function* () {
|
||||
yield* svc.updateMessage(msg)
|
||||
yield* svc.updatePart({
|
||||
id: pid,
|
||||
messageID: msg.id,
|
||||
sessionID: next.id,
|
||||
type: "text",
|
||||
text: compose(""),
|
||||
synthetic: false,
|
||||
} satisfies MessageV2.TextPart)
|
||||
}),
|
||||
)
|
||||
|
||||
if (todos.length) {
|
||||
await PlanFollowupRuntime.todo.update({ sessionID: next.id, todos })
|
||||
@@ -415,14 +427,16 @@ export namespace PlanFollowup {
|
||||
}
|
||||
|
||||
if (handover) {
|
||||
await Session.updatePart({
|
||||
id: pid,
|
||||
messageID: msg.id,
|
||||
sessionID: next.id,
|
||||
type: "text",
|
||||
text: compose(handover),
|
||||
synthetic: false,
|
||||
} satisfies MessageV2.TextPart)
|
||||
await PlanFollowupRuntime.session((svc) =>
|
||||
svc.updatePart({
|
||||
id: pid,
|
||||
messageID: msg.id,
|
||||
sessionID: next.id,
|
||||
type: "text",
|
||||
text: compose(handover),
|
||||
synthetic: false,
|
||||
} satisfies MessageV2.TextPart),
|
||||
)
|
||||
}
|
||||
if (ctl.signal.aborted) {
|
||||
await idle()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Effect } from "effect"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { SessionID, PartID } from "@/session/schema"
|
||||
@@ -22,45 +23,47 @@ function childID(part: MessageV2.Part): string | undefined {
|
||||
* child session references, causing SSE events and permission prompts to bleed
|
||||
* across sessions.
|
||||
*/
|
||||
export async function remapChildren(sid: SessionID): Promise<void> {
|
||||
const msgs = await Session.messages({ sessionID: sid })
|
||||
const refs: { part: MessageV2.ToolPart; child: string }[] = []
|
||||
for (const msg of msgs) {
|
||||
for (const part of msg.parts) {
|
||||
const child = childID(part)
|
||||
if (child) refs.push({ part: part as MessageV2.ToolPart, child })
|
||||
export function remapChildren(sid: SessionID): Effect.Effect<void, Session.NotFound, Session.Service> {
|
||||
return Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const msgs = yield* sessions.messages({ sessionID: sid })
|
||||
const refs: { part: MessageV2.ToolPart; child: string }[] = []
|
||||
for (const msg of msgs) {
|
||||
for (const part of msg.parts) {
|
||||
const child = childID(part)
|
||||
if (child) refs.push({ part: part as MessageV2.ToolPart, child })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (refs.length === 0) return
|
||||
if (refs.length === 0) return
|
||||
|
||||
const remapped = new Map<string, SessionID>()
|
||||
for (const ref of refs) {
|
||||
if (remapped.has(ref.child)) continue
|
||||
const exists = await Session.get(SessionID.make(ref.child)).catch(() => undefined)
|
||||
if (!exists) continue
|
||||
// Session.fork() already calls remapChildren on the forked child,
|
||||
// so nested subagents are handled recursively without an explicit call here.
|
||||
const forked = await Session.fork({ sessionID: SessionID.make(ref.child) })
|
||||
remapped.set(ref.child, forked.id)
|
||||
}
|
||||
const remapped = new Map<string, SessionID>()
|
||||
for (const ref of refs) {
|
||||
if (remapped.has(ref.child)) continue
|
||||
const exists = yield* sessions.get(SessionID.make(ref.child)).pipe(Effect.orElseSucceed(() => undefined))
|
||||
if (!exists) continue
|
||||
const forked = yield* sessions.fork({ sessionID: SessionID.make(ref.child) })
|
||||
yield* remapChildren(forked.id)
|
||||
remapped.set(ref.child, forked.id)
|
||||
}
|
||||
|
||||
if (remapped.size === 0) return
|
||||
if (remapped.size === 0) return
|
||||
|
||||
for (const ref of refs) {
|
||||
const replacement = remapped.get(ref.child)
|
||||
if (!replacement) continue
|
||||
const meta = (ref.part.state as { metadata?: Record<string, unknown> }).metadata
|
||||
if (!meta) continue
|
||||
await Session.updatePart({
|
||||
...ref.part,
|
||||
id: PartID.make(ref.part.id),
|
||||
sessionID: SessionID.make(ref.part.sessionID),
|
||||
state: {
|
||||
...ref.part.state,
|
||||
metadata: { ...meta, sessionId: replacement },
|
||||
},
|
||||
} as MessageV2.ToolPart)
|
||||
}
|
||||
for (const ref of refs) {
|
||||
const replacement = remapped.get(ref.child)
|
||||
if (!replacement) continue
|
||||
const meta = (ref.part.state as { metadata?: Record<string, unknown> }).metadata
|
||||
if (!meta) continue
|
||||
yield* sessions.updatePart({
|
||||
...ref.part,
|
||||
id: PartID.make(ref.part.id),
|
||||
sessionID: SessionID.make(ref.part.sessionID),
|
||||
state: {
|
||||
...ref.part.state,
|
||||
metadata: { ...meta, sessionId: replacement },
|
||||
},
|
||||
} as MessageV2.ToolPart)
|
||||
}
|
||||
|
||||
log.info("remapped child sessions", { session: sid, count: remapped.size })
|
||||
log.info("remapped child sessions", { session: sid, count: remapped.size })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// kilocode_change - new file
|
||||
import { remapChildren as _remapChildren } from "./fork"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageID, SessionID } from "@/session/schema"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Database, eq, and, gte, isNull, desc, like, inArray, lt, or } from "@/storage/db"
|
||||
import type { SQL } from "@/storage/db"
|
||||
@@ -397,9 +396,14 @@ export namespace KiloSession {
|
||||
export const kiloSessionFork = fn(
|
||||
z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() }),
|
||||
async (input) => {
|
||||
const { runPromise } = makeRuntime(Session.Service, Session.defaultLayer)
|
||||
const session = await runPromise((svc) => svc.fork(input))
|
||||
await KiloSession.remapChildren(session.id)
|
||||
return session
|
||||
const { AppRuntime } = await import("@/effect/app-runtime")
|
||||
return AppRuntime.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.fork(input)
|
||||
yield* KiloSession.remapChildren(session.id)
|
||||
return session
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as path from "path"
|
||||
import { Readable } from "stream"
|
||||
import * as Encoding from "../encoding"
|
||||
|
||||
type ObjectValue = Record<string, unknown>
|
||||
|
||||
const object = (value: unknown): value is ObjectValue => typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
|
||||
const parse = (text: string): unknown => {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const source = (value: unknown): string | undefined => {
|
||||
if (typeof value === "string") return value
|
||||
if (!Array.isArray(value) || !value.every((line) => typeof line === "string")) return undefined
|
||||
return value.join("")
|
||||
}
|
||||
|
||||
const render = (kind: "markdown" | "code", text: string) => {
|
||||
const body = text.endsWith("\n") ? text : `${text}\n`
|
||||
return `<${kind}_cell>\n${body}</${kind}_cell>`
|
||||
}
|
||||
|
||||
export function isFile(filepath: string) {
|
||||
return path.extname(filepath).toLowerCase() === ".ipynb"
|
||||
}
|
||||
|
||||
export async function open(filepath: string): Promise<Readable> {
|
||||
const raw = (await Encoding.read(filepath)).text
|
||||
const data = parse(raw)
|
||||
if (!object(data) || !Array.isArray(data.cells)) return Readable.from([raw])
|
||||
|
||||
const cells: string[] = []
|
||||
for (const cell of data.cells) {
|
||||
if (!object(cell)) continue
|
||||
if (cell.cell_type !== "markdown" && cell.cell_type !== "code") continue
|
||||
|
||||
const text = source(cell.source)
|
||||
if (text === undefined) continue
|
||||
cells.push(render(cell.cell_type, text))
|
||||
}
|
||||
|
||||
return Readable.from([cells.length ? cells.join("\n\n") : "(Notebook contains no markdown or code cell content.)"])
|
||||
}
|
||||
@@ -3,13 +3,19 @@ import { Readable } from "stream"
|
||||
import { read, utils, type CellObject, type WorkBook } from "xlsx"
|
||||
|
||||
const ROW_LIMIT = 50_000
|
||||
const MAX_SIZE = 50 * 1024 * 1024
|
||||
const MAX_SIZE_LABEL = `${MAX_SIZE / (1024 * 1024)} MB`
|
||||
|
||||
export function is(filepath: string) {
|
||||
return path.extname(filepath).toLowerCase() === ".xlsx"
|
||||
}
|
||||
|
||||
export async function open(filepath: string) {
|
||||
const bytes = new Uint8Array(await Bun.file(filepath).arrayBuffer())
|
||||
const file = Bun.file(filepath)
|
||||
if (file.size > MAX_SIZE) {
|
||||
throw new Error(`Cannot read spreadsheet file: ${filepath} exceeds the ${MAX_SIZE_LABEL} size limit`)
|
||||
}
|
||||
const bytes = new Uint8Array(await file.arrayBuffer())
|
||||
if (bytes[0] !== 0x50 || bytes[1] !== 0x4b) {
|
||||
throw new Error(`Cannot read spreadsheet file: ${filepath} is not a valid XLSX workbook`)
|
||||
}
|
||||
|
||||
@@ -29,12 +29,9 @@ import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { Permission } from "@/permission"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
// kilocode_change start - legacy promise helpers + kilocode extensions
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
// kilocode_change start - Kilo session behavior extensions
|
||||
import { BackgroundProcess } from "@/kilocode/background-process"
|
||||
import { KiloSession, kiloSessionFork } from "@/kilocode/session"
|
||||
import { fn } from "@/util/fn"
|
||||
import { z } from "zod"
|
||||
// kilocode_change end
|
||||
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
@@ -949,69 +946,7 @@ export function* listGlobal(input?: {
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - keep legacy promise helpers for Kilo callsites
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
const decodeCreate = Schema.decodeUnknownSync(CreateInput)
|
||||
const decodeGet = Schema.decodeUnknownSync(GetInput)
|
||||
const decodeSetTitle = Schema.decodeUnknownSync(SetTitleInput)
|
||||
const decodeSetArchived = Schema.decodeUnknownSync(SetArchivedInput)
|
||||
const decodeSetPermission = Schema.decodeUnknownSync(SetPermissionInput)
|
||||
const decodeSetRevert = Schema.decodeUnknownSync(SetRevertInput)
|
||||
const decodeMessages = Schema.decodeUnknownSync(MessagesInput)
|
||||
const decodeChildren = Schema.decodeUnknownSync(ChildrenInput)
|
||||
const decodeRemove = Schema.decodeUnknownSync(RemoveInput)
|
||||
|
||||
export const create = (input?: CreateInput) => runPromise((svc) => svc.create(decodeCreate(input) as CreateInput))
|
||||
// kilocode_change - preserve Kilo recursive fork/remap behavior without a Session service-local Promise runtime
|
||||
export const fork = kiloSessionFork
|
||||
export const get = (id: SessionID) => runPromise((svc) => svc.get(decodeGet(id)))
|
||||
export const setTitle = (input: { sessionID: SessionID; title: string }) =>
|
||||
runPromise((svc) => svc.setTitle(decodeSetTitle(input)))
|
||||
export const setArchived = (input: { sessionID: SessionID; time?: number }) =>
|
||||
runPromise((svc) => svc.setArchived(decodeSetArchived(input)))
|
||||
export const setPermission = (input: { sessionID: SessionID; permission: Permission.Ruleset }) =>
|
||||
runPromise((svc) =>
|
||||
svc.setPermission(decodeSetPermission(input) as { sessionID: SessionID; permission: Permission.Ruleset }),
|
||||
)
|
||||
export const setRevert = (input: { sessionID: SessionID; revert?: Info["revert"]; summary?: Info["summary"] }) => {
|
||||
const parsed = decodeSetRevert(input) as { sessionID: SessionID; revert?: Info["revert"]; summary?: Info["summary"] }
|
||||
return runPromise((svc) =>
|
||||
svc.setRevert({ sessionID: parsed.sessionID, revert: parsed.revert, summary: parsed.summary }),
|
||||
)
|
||||
}
|
||||
export const messages = (input: { sessionID: SessionID; limit?: number }) =>
|
||||
runPromise((svc) => svc.messages(decodeMessages(input)))
|
||||
export const children = (id: SessionID) => runPromise((svc) => svc.children(decodeChildren(id)))
|
||||
export const remove = (id: SessionID) => runPromise((svc) => svc.remove(decodeRemove(id)))
|
||||
export async function updateMessage<T extends MessageV2.Info>(msg: T): Promise<T> {
|
||||
MessageV2.Info.zod.parse(msg) // kilocode_change
|
||||
return runPromise((svc) => svc.updateMessage(msg))
|
||||
}
|
||||
|
||||
export const removeMessage = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod }), (input) =>
|
||||
runPromise((svc) => svc.removeMessage(input)),
|
||||
)
|
||||
|
||||
export const removePart = fn(
|
||||
z.object({ sessionID: SessionID.zod, messageID: MessageID.zod, partID: PartID.zod }),
|
||||
(input) => runPromise((svc) => svc.removePart(input)),
|
||||
)
|
||||
|
||||
export async function updatePart<T extends MessageV2.Part>(part: T): Promise<T> {
|
||||
MessageV2.Part.zod.parse(part) // kilocode_change
|
||||
return runPromise((svc) => svc.updatePart(part))
|
||||
}
|
||||
|
||||
export const updatePartDelta = fn(
|
||||
z.object({
|
||||
sessionID: SessionID.zod,
|
||||
messageID: MessageID.zod,
|
||||
partID: PartID.zod,
|
||||
field: z.string(),
|
||||
delta: z.string(),
|
||||
}),
|
||||
(input) => runPromise((svc) => svc.updatePartDelta(input)),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
export * as Session from "./session"
|
||||
|
||||
@@ -15,6 +15,7 @@ import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
|
||||
// kilocode_change start
|
||||
import * as Encoding from "../kilocode/encoding"
|
||||
import * as TextStream from "../kilocode/text-stream"
|
||||
import * as Notebook from "../kilocode/tool/notebook"
|
||||
import * as Xlsx from "../kilocode/tool/xlsx"
|
||||
// kilocode_change end
|
||||
|
||||
@@ -364,6 +365,7 @@ export const ReadTool = Tool.define(
|
||||
// routed through TextStream.withFallback so non-UTF-8 files are decoded via
|
||||
// iconv. The body otherwise matches upstream.
|
||||
export async function lines(filepath: string, opts: { limit: number; offset: number }) {
|
||||
if (Notebook.isFile(filepath)) return readLines(await Notebook.open(filepath), opts) // kilocode_change - extract readable notebook cells before paging
|
||||
return TextStream.withFallback(filepath, (stream) => readLines(stream, opts))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Instance } from "../project/instance"
|
||||
import { Locale } from "../util/locale"
|
||||
import { Filesystem } from "../util/filesystem" // kilocode_change
|
||||
import { WorktreeFamily } from "../kilocode/worktree-family" // kilocode_change
|
||||
import { Session } from "../session/session" // kilocode_change
|
||||
import { SessionID } from "../session/schema" // kilocode_change
|
||||
import DESCRIPTION from "./recall.txt"
|
||||
|
||||
const Parameters = Schema.Struct({
|
||||
@@ -28,6 +30,7 @@ export const RecallTool = Tool.define(
|
||||
"kilo_local_recall",
|
||||
Effect.gen(function* () {
|
||||
const git = yield* Git.Service
|
||||
const sessions = yield* Session.Service // kilocode_change
|
||||
return {
|
||||
description: DESCRIPTION,
|
||||
parameters: Parameters,
|
||||
@@ -37,7 +40,7 @@ export const RecallTool = Tool.define(
|
||||
if (params.mode === "search") {
|
||||
return yield* Effect.promise(() => search(params, ctx, bridge, git))
|
||||
}
|
||||
return yield* Effect.promise(() => read(params, ctx, bridge, git))
|
||||
return yield* Effect.promise(() => read(params, ctx, bridge, git, sessions))
|
||||
}).pipe(Effect.orDie),
|
||||
}
|
||||
}),
|
||||
@@ -106,14 +109,18 @@ async function search(
|
||||
}
|
||||
}
|
||||
|
||||
async function read(params: { sessionID?: string }, ctx: Tool.Context, bridge: EffectBridge.Shape, git: Git.Interface) {
|
||||
async function read(
|
||||
params: { sessionID?: string },
|
||||
ctx: Tool.Context,
|
||||
bridge: EffectBridge.Shape,
|
||||
git: Git.Interface,
|
||||
sessions: Session.Interface,
|
||||
) {
|
||||
if (!params.sessionID) {
|
||||
throw new Error("The 'sessionID' parameter is required when mode is 'read'")
|
||||
}
|
||||
|
||||
const { Session } = await import("../session/session") // kilocode_change
|
||||
const { SessionID } = await import("../session/schema") // kilocode_change
|
||||
const session = await Session.get(SessionID.make(params.sessionID)).catch(() => {
|
||||
const session = await bridge.promise(sessions.get(SessionID.make(params.sessionID))).catch(() => {
|
||||
throw new Error(`Session "${params.sessionID}" not found. Use search mode first to find valid session IDs.`)
|
||||
})
|
||||
const dirs = await bridge.promise(WorktreeFamily.list().pipe(Effect.provideService(Git.Service, git))) // kilocode_change
|
||||
@@ -140,7 +147,7 @@ async function read(params: { sessionID?: string }, ctx: Tool.Context, bridge: E
|
||||
})
|
||||
}
|
||||
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await bridge.promise(sessions.messages({ sessionID: session.id }))
|
||||
const lines: string[] = [
|
||||
`# Session: ${session.title}`,
|
||||
`Directory: ${session.directory}`,
|
||||
|
||||
@@ -16,7 +16,11 @@ const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
||||
|
||||
function layer(overrides: Partial<Config.Interface> = {}) {
|
||||
return Layer.merge(
|
||||
KiloSessions.layer.pipe(Layer.provideMerge(Bus.layer), Layer.provide(TestConfig.layer(overrides))),
|
||||
KiloSessions.layer.pipe(
|
||||
Layer.provideMerge(Bus.layer),
|
||||
Layer.provide(TestConfig.layer(overrides)),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
),
|
||||
Auth.defaultLayer,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
@@ -16,6 +17,19 @@ import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
get: (id: SessionID) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.get(id)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
const model = {
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
@@ -32,8 +46,8 @@ async function seed(input: {
|
||||
tools?: Array<{ tool: string; input: Record<string, unknown>; output: string }>
|
||||
finish?: string
|
||||
}) {
|
||||
const session = await Session.create({})
|
||||
const user = await Session.updateMessage({
|
||||
const session = await sessions.create({})
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -41,7 +55,7 @@ async function seed(input: {
|
||||
agent: input.agent ?? "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -73,9 +87,9 @@ async function seed(input: {
|
||||
},
|
||||
finish: (input.finish as MessageV2.Assistant["finish"]) ?? "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant)
|
||||
await sessions.updateMessage(assistant)
|
||||
if (input.text !== undefined) {
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -85,7 +99,7 @@ async function seed(input: {
|
||||
}
|
||||
|
||||
for (const t of input.tools ?? []) {
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -103,7 +117,7 @@ async function seed(input: {
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
return { sessionID: session.id, messages }
|
||||
}
|
||||
|
||||
@@ -217,7 +231,7 @@ describe("plan_exit detection", () => {
|
||||
|
||||
await expect(pending).resolves.toBe("continue")
|
||||
|
||||
const messages = await Session.messages({ sessionID: seeded.sessionID })
|
||||
const messages = await sessions.messages({ sessionID: seeded.sessionID })
|
||||
const user = messages
|
||||
.slice()
|
||||
.reverse()
|
||||
@@ -239,8 +253,8 @@ describe("plan_exit detection", () => {
|
||||
|
||||
test("plan_exit with non-completed status does NOT trigger", () =>
|
||||
withInstance(async () => {
|
||||
const session = await Session.create({})
|
||||
const user = await Session.updateMessage({
|
||||
const session = await sessions.create({})
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -248,7 +262,7 @@ describe("plan_exit detection", () => {
|
||||
agent: "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -277,15 +291,15 @@ describe("plan_exit detection", () => {
|
||||
},
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
type: "text",
|
||||
text: "Here is the plan",
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -301,7 +315,7 @@ describe("plan_exit detection", () => {
|
||||
},
|
||||
} satisfies MessageV2.ToolPart)
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
|
||||
// Verify the tool part IS present but errored (not completed)
|
||||
const toolPart = messages.flatMap((msg) => msg.parts).find((p) => p.type === "tool" && p.tool === "plan_exit")
|
||||
@@ -318,10 +332,10 @@ describe("plan_exit detection", () => {
|
||||
|
||||
test("plan_exit on earlier assistant message triggers when later message has text only", () =>
|
||||
withInstance(async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await sessions.create({})
|
||||
// Use explicit timestamps to ensure deterministic message ordering
|
||||
const now = Date.now()
|
||||
const user = await Session.updateMessage({
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -329,7 +343,7 @@ describe("plan_exit detection", () => {
|
||||
agent: "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -353,8 +367,8 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "tool-calls",
|
||||
}
|
||||
await Session.updateMessage(assistant1)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant1)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant1.id,
|
||||
sessionID: session.id,
|
||||
@@ -387,8 +401,8 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant2)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant2)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant2.id,
|
||||
sessionID: session.id,
|
||||
@@ -396,7 +410,7 @@ describe("plan_exit detection", () => {
|
||||
text: "The plan is complete. I've called plan_exit.",
|
||||
})
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
expect(SessionPrompt.shouldAskPlanFollowup({ messages, abort: AbortSignal.any([]) })).toBe(true)
|
||||
}))
|
||||
|
||||
@@ -412,7 +426,7 @@ describe("plan_exit detection", () => {
|
||||
],
|
||||
})
|
||||
|
||||
const session = await Session.get(seeded.sessionID)
|
||||
const session = await sessions.get(seeded.sessionID)
|
||||
const plan = Session.plan(session, Instance.current)
|
||||
await fs.mkdir(path.dirname(plan), { recursive: true })
|
||||
await Bun.write(plan, "Do implementation step 1")
|
||||
@@ -435,10 +449,10 @@ describe("plan_exit detection", () => {
|
||||
|
||||
test("PlanFollowup.ask shows prompt when plan text is on earlier assistant and last assistant is empty", () =>
|
||||
withInstance(async () => {
|
||||
const session = await Session.create({})
|
||||
const session = await sessions.create({})
|
||||
// Use explicit timestamps to ensure deterministic message ordering
|
||||
const now = Date.now()
|
||||
const user = await Session.updateMessage({
|
||||
const user = await sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -446,7 +460,7 @@ describe("plan_exit detection", () => {
|
||||
agent: "plan",
|
||||
model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -470,15 +484,15 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "tool-calls",
|
||||
}
|
||||
await Session.updateMessage(assistant1)
|
||||
await Session.updatePart({
|
||||
await sessions.updateMessage(assistant1)
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant1.id,
|
||||
sessionID: session.id,
|
||||
type: "text",
|
||||
text: "Here is the detailed plan:\n\n## Step 1\nDo something\n\n## Step 2\nDo something else",
|
||||
})
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant1.id,
|
||||
sessionID: session.id,
|
||||
@@ -511,9 +525,9 @@ describe("plan_exit detection", () => {
|
||||
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant2)
|
||||
await sessions.updateMessage(assistant2)
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
|
||||
// shouldAskPlanFollowup should detect plan_exit on the earlier message
|
||||
expect(SessionPrompt.shouldAskPlanFollowup({ messages, abort: AbortSignal.any([]) })).toBe(true)
|
||||
|
||||
@@ -46,6 +46,19 @@ const todo = {
|
||||
},
|
||||
}
|
||||
|
||||
const store = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
get: (id: SessionID) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.get(id)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
const model = {
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
@@ -90,8 +103,8 @@ async function seed(input: {
|
||||
variant?: string
|
||||
tools?: Array<{ tool: string; input: Record<string, unknown>; output: string }>
|
||||
}) {
|
||||
const session = await Session.create({})
|
||||
const user = await Session.updateMessage({
|
||||
const session = await store.create({})
|
||||
const user = await store.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: session.id,
|
||||
@@ -101,7 +114,7 @@ async function seed(input: {
|
||||
agent: "plan",
|
||||
model: input.variant ? { ...model, variant: input.variant } : model,
|
||||
})
|
||||
await Session.updatePart({
|
||||
await store.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: user.id,
|
||||
sessionID: session.id,
|
||||
@@ -138,8 +151,8 @@ async function seed(input: {
|
||||
},
|
||||
finish: "end_turn",
|
||||
}
|
||||
await Session.updateMessage(assistant)
|
||||
await Session.updatePart({
|
||||
await store.updateMessage(assistant)
|
||||
await store.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -148,7 +161,7 @@ async function seed(input: {
|
||||
})
|
||||
|
||||
for (const t of input.tools ?? []) {
|
||||
await Session.updatePart({
|
||||
await store.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistant.id,
|
||||
sessionID: session.id,
|
||||
@@ -166,7 +179,7 @@ async function seed(input: {
|
||||
} satisfies MessageV2.ToolPart)
|
||||
}
|
||||
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await store.messages({ sessionID: session.id })
|
||||
return {
|
||||
sessionID: session.id,
|
||||
messages,
|
||||
@@ -174,7 +187,7 @@ async function seed(input: {
|
||||
}
|
||||
|
||||
async function latestUser(sessionID: SessionID) {
|
||||
const messages = await Session.messages({ sessionID })
|
||||
const messages = await store.messages({ sessionID })
|
||||
return messages
|
||||
.slice()
|
||||
.reverse()
|
||||
@@ -500,7 +513,7 @@ describe("plan follow-up", () => {
|
||||
await expect(pending).resolves.toBe("continue")
|
||||
|
||||
// The injected user message must be visible when scoped
|
||||
const all = await Session.messages({ sessionID: seeded.sessionID })
|
||||
const all = await store.messages({ sessionID: seeded.sessionID })
|
||||
const scoped = KiloSessionPromptQueue.scope(seeded.sessionID, all)
|
||||
const injected = scoped.findLast((m) => m.info.role === "user")
|
||||
expect(injected).toBeDefined()
|
||||
@@ -629,8 +642,8 @@ describe("plan follow-up", () => {
|
||||
if (!newSessionID || !next) throw new Error("expected follow-up session")
|
||||
expect(next.id).toBe(newSessionID)
|
||||
expect(next.parentID).toBeUndefined()
|
||||
const planPath = Session.plan(await Session.get(seeded.sessionID), Instance.current)
|
||||
const messages = await Session.messages({ sessionID: newSessionID })
|
||||
const planPath = Session.plan(await store.get(seeded.sessionID), Instance.current)
|
||||
const messages = await store.messages({ sessionID: newSessionID })
|
||||
const user = messages.find((item) => item.info.role === "user")
|
||||
expect(user?.info.role).toBe("user")
|
||||
if (!user || user.info.role !== "user") throw new Error("expected seeded user message")
|
||||
@@ -737,9 +750,9 @@ describe("plan follow-up", () => {
|
||||
if (next) {
|
||||
const planPath = await WithInstance.provide({
|
||||
directory: dir,
|
||||
fn: async () => Session.plan(await Session.get(seeded.sessionID), Instance.current),
|
||||
fn: async () => Session.plan(await store.get(seeded.sessionID), Instance.current),
|
||||
})
|
||||
const messages = await Session.messages({ sessionID: next.id })
|
||||
const messages = await store.messages({ sessionID: next.id })
|
||||
const user = messages.find((item) => item.info.role === "user")
|
||||
if (!user || user.info.role !== "user") throw new Error("expected user message")
|
||||
const part = user.parts.find((item) => item.type === "text")
|
||||
@@ -947,7 +960,7 @@ describe("plan follow-up", () => {
|
||||
|
||||
const newSessionID = created[0]
|
||||
if (!newSessionID) throw new Error("expected follow-up session")
|
||||
const messages = await Session.messages({ sessionID: newSessionID })
|
||||
const messages = await store.messages({ sessionID: newSessionID })
|
||||
const user = messages.find((item) => item.info.role === "user")
|
||||
if (!user || user.info.role !== "user") throw new Error("expected user message")
|
||||
const part = user.parts.find((item) => item.type === "text")
|
||||
@@ -1111,7 +1124,7 @@ describe("plan follow-up", () => {
|
||||
// deferred has not resolved yet.
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (followup) {
|
||||
const msgs = await Session.messages({ sessionID: followup })
|
||||
const msgs = await store.messages({ sessionID: followup })
|
||||
const user = msgs.find((m) => m.info.role === "user")
|
||||
const part = user?.parts.find((p) => p.type === "text")
|
||||
if (part?.type === "text" && part.text.includes("Implement the following plan:")) break
|
||||
@@ -1121,7 +1134,7 @@ describe("plan follow-up", () => {
|
||||
|
||||
expect(followup).toBeDefined()
|
||||
if (!followup) return
|
||||
const initial = await Session.messages({ sessionID: followup })
|
||||
const initial = await store.messages({ sessionID: followup })
|
||||
const initialUser = initial.find((m) => m.info.role === "user")
|
||||
const initialPart = initialUser?.parts.find((p) => p.type === "text")
|
||||
expect(initialPart?.type).toBe("text")
|
||||
@@ -1135,7 +1148,7 @@ describe("plan follow-up", () => {
|
||||
await expect(pending).resolves.toBe("break")
|
||||
|
||||
// Same part ID updated in-place — handover section now present.
|
||||
const final = await Session.messages({ sessionID: followup })
|
||||
const final = await store.messages({ sessionID: followup })
|
||||
const finalUser = final.find((m) => m.info.role === "user")
|
||||
const finalPart = finalUser?.parts.find((p) => p.type === "text")
|
||||
if (finalPart?.type !== "text") return
|
||||
@@ -1294,7 +1307,7 @@ describe("plan follow-up", () => {
|
||||
})
|
||||
|
||||
await expect(pending).resolves.toBe("break")
|
||||
expect((await Session.messages({ sessionID: seeded.sessionID })).length).toBe(2)
|
||||
expect((await store.messages({ sessionID: seeded.sessionID })).length).toBe(2)
|
||||
}))
|
||||
|
||||
test("formatTodos - returns empty string for no todos", () => {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import path from "path"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LSP } from "../../src/lsp/lsp"
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { ReadTool } from "../../src/tool/read"
|
||||
import * as Tool from "../../src/tool/tool"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const ctx = {
|
||||
sessionID: SessionID.make("ses_test-notebook"),
|
||||
messageID: MessageID.make(""),
|
||||
callID: "",
|
||||
agent: "code",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
Agent.defaultLayer,
|
||||
AppFileSystem.defaultLayer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
Instruction.defaultLayer,
|
||||
LSP.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
const run = Effect.fn("NotebookReadTest.run")(function* (dir: string, args: Tool.InferParameters<typeof ReadTool>) {
|
||||
return yield* provideInstance(dir)(
|
||||
Effect.gen(function* () {
|
||||
const info = yield* ReadTool
|
||||
const tool = yield* Tool.init(info)
|
||||
return yield* tool.execute(args, ctx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const put = Effect.fn("NotebookReadTest.put")(function* (filepath: string, content: string | Uint8Array) {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
yield* fs.writeWithDirs(filepath, content)
|
||||
})
|
||||
|
||||
const notebook = JSON.stringify({
|
||||
metadata: { secret: "ignore-notebook-metadata" },
|
||||
cells: [
|
||||
{
|
||||
cell_type: "markdown",
|
||||
metadata: { private: "ignore-cell-metadata" },
|
||||
source: ["# Analysis\n", "Useful introduction"],
|
||||
},
|
||||
{
|
||||
cell_type: "raw",
|
||||
source: ["ignore raw cell"],
|
||||
},
|
||||
{
|
||||
cell_type: "code",
|
||||
execution_count: 7,
|
||||
metadata: {},
|
||||
source: ["value = 42\n", "print(value)"],
|
||||
outputs: [{ output_type: "stream", text: ["ignore-output-payload"] }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
describe("kilocode notebook reads", () => {
|
||||
it.live("extracts markdown and code cells without notebook payloads", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "analysis.ipynb")
|
||||
yield* put(filepath, notebook)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("<markdown_cell>")
|
||||
expect(result.output).toContain("# Analysis")
|
||||
expect(result.output).toContain("<code_cell>")
|
||||
expect(result.output).toContain("value = 42")
|
||||
expect(result.output.indexOf("# Analysis")).toBeLessThan(result.output.indexOf("value = 42"))
|
||||
expect(result.output).not.toContain("ignore-output-payload")
|
||||
expect(result.output).not.toContain("ignore-notebook-metadata")
|
||||
expect(result.output).not.toContain("ignore-cell-metadata")
|
||||
expect(result.output).not.toContain("ignore raw cell")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("skips invalid cells without exposing raw notebook payloads", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "partial.ipynb")
|
||||
const content = JSON.stringify({
|
||||
cells: [
|
||||
null,
|
||||
{ cell_type: "code", source: null, outputs: ["INVALID_OUTPUT_SHOULD_NOT_APPEAR"] },
|
||||
{ cell_type: "markdown", source: ["Readable cell"], metadata: { marker: "CELL_METADATA_SHOULD_NOT_APPEAR" } },
|
||||
],
|
||||
})
|
||||
yield* put(filepath, content)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("Readable cell")
|
||||
expect(result.output).not.toContain("INVALID_OUTPUT_SHOULD_NOT_APPEAR")
|
||||
expect(result.output).not.toContain("CELL_METADATA_SHOULD_NOT_APPEAR")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("reports valid notebooks with no readable cells without exposing payloads", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "empty-content.ipynb")
|
||||
const content = JSON.stringify({
|
||||
metadata: { marker: "NOTEBOOK_METADATA_SHOULD_NOT_APPEAR" },
|
||||
cells: [
|
||||
null,
|
||||
{ cell_type: "raw", source: ["RAW_CONTENT_SHOULD_NOT_APPEAR"] },
|
||||
{ cell_type: "code", source: null, outputs: ["INVALID_OUTPUT_SHOULD_NOT_APPEAR"] },
|
||||
],
|
||||
})
|
||||
yield* put(filepath, content)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("Notebook contains no markdown or code cell content")
|
||||
expect(result.output).not.toContain("NOTEBOOK_METADATA_SHOULD_NOT_APPEAR")
|
||||
expect(result.output).not.toContain("RAW_CONTENT_SHOULD_NOT_APPEAR")
|
||||
expect(result.output).not.toContain("INVALID_OUTPUT_SHOULD_NOT_APPEAR")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("applies read pagination to extracted cell text", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "paged.ipynb")
|
||||
yield* put(filepath, notebook)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath, offset: 2, limit: 2 })
|
||||
|
||||
expect(result.output).toContain("2: # Analysis")
|
||||
expect(result.output).toContain("3: Useful introduction")
|
||||
expect(result.output).not.toContain("value = 42")
|
||||
expect(result.metadata.preview).toBe("# Analysis\nUseful introduction")
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("falls back to raw text for malformed notebooks", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "broken.ipynb")
|
||||
const content = '{"cells":[{"cell_type":"markdown","source":["unfinished"]}'
|
||||
yield* put(filepath, content)
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain(content)
|
||||
expect(result.output).not.toContain("<markdown_cell>")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("keeps ordinary text reads unchanged", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "notes.txt")
|
||||
yield* put(filepath, "plain text")
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toContain("1: plain text")
|
||||
expect(result.output).not.toContain("<markdown_cell>")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("keeps PDF files as native attachments", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const filepath = path.join(dir, "document.pdf")
|
||||
yield* put(filepath, "%PDF-1.4\nminimal content")
|
||||
|
||||
const result = yield* run(dir, { filePath: filepath })
|
||||
|
||||
expect(result.output).toBe("PDF read successfully")
|
||||
expect(result.attachments?.[0].mime).toBe("application/pdf")
|
||||
expect(result.metadata.truncated).toBe(false)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { truncate } from "fs/promises"
|
||||
import path from "path"
|
||||
import { write, utils, type WorkBook, type WorkSheet } from "xlsx"
|
||||
import { TextReader, TextWriter, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } from "@zip.js/zip.js"
|
||||
@@ -198,6 +199,20 @@ describe("kilocode XLSX reads", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("rejects spreadsheets larger than the parser input limit", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const file = path.join(dir, "large-input.xlsx")
|
||||
yield* put(file, new Uint8Array([0x50, 0x4b]))
|
||||
yield* Effect.promise(() => truncate(file, 50 * 1024 * 1024 + 1))
|
||||
|
||||
const err = yield* fail(dir, file)
|
||||
|
||||
expect(err.message).toContain("Cannot read spreadsheet file")
|
||||
expect(err.message).toContain("exceeds the 50 MB size limit")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("continues rejecting unsupported workbook formats as binary", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
|
||||
@@ -17,6 +17,7 @@ const bus = Bus.layer
|
||||
const env = Layer.mergeAll(
|
||||
Permission.layer.pipe(Layer.provide(bus), Layer.provide(Config.defaultLayer)),
|
||||
Config.defaultLayer,
|
||||
Session.defaultLayer,
|
||||
bus,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
)
|
||||
@@ -72,10 +73,11 @@ describe("AllowEverythingPermission", () => {
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const sessions = yield* Session.Service
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: true })).toBe(true)
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: false })).toBe(true)
|
||||
|
||||
const session = yield* Effect.promise(() => Session.create({}))
|
||||
const session = yield* sessions.create({})
|
||||
const pending = yield* ask({
|
||||
id: PermissionID.make("permission_global_disable"),
|
||||
sessionID: session.id,
|
||||
@@ -106,16 +108,15 @@ describe("AllowEverythingPermission", () => {
|
||||
provideTmpdirInstance(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Effect.promise(() =>
|
||||
Session.create({
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
}),
|
||||
)
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
})
|
||||
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: true, sessionID: session.id })).toBe(true)
|
||||
expect(yield* AllowEverythingPermission.effect({ enable: false, sessionID: session.id })).toBe(true)
|
||||
|
||||
const next = yield* Effect.promise(() => Session.get(session.id))
|
||||
const next = yield* sessions.get(session.id)
|
||||
expect(next.permission ?? []).toEqual([])
|
||||
|
||||
const pending = yield* ask({
|
||||
@@ -140,7 +141,7 @@ describe("AllowEverythingPermission", () => {
|
||||
expect(Cause.squash(exit.cause)).toBeInstanceOf(Permission.RejectedError)
|
||||
}
|
||||
|
||||
const other = yield* Effect.promise(() => Session.create({}))
|
||||
const other = yield* sessions.create({})
|
||||
const blocked = yield* ask({
|
||||
id: PermissionID.make("permission_other_session"),
|
||||
sessionID: other.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
@@ -8,6 +9,19 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
get: (id: SessionID) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.get(id)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updatePart: <T extends MessageV2.Part>(part: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await disposeAllInstances()
|
||||
})
|
||||
@@ -36,7 +50,7 @@ function taskPart(input: { messageID: string; sessionID: string; childSessionID:
|
||||
|
||||
async function userMsg(sid: string) {
|
||||
const id = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
await sessions.updateMessage({
|
||||
id,
|
||||
sessionID: SessionID.make(sid),
|
||||
role: "user",
|
||||
@@ -50,7 +64,7 @@ async function userMsg(sid: string) {
|
||||
|
||||
async function asstMsg(sid: string, parent: string) {
|
||||
const id = MessageID.ascending()
|
||||
await Session.updateMessage({
|
||||
await sessions.updateMessage({
|
||||
id,
|
||||
sessionID: SessionID.make(sid),
|
||||
role: "assistant",
|
||||
@@ -75,12 +89,12 @@ describe("Session.fork child session remapping", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const parent = await Session.create({ title: "parent" })
|
||||
const child = await Session.create({ parentID: parent.id, title: "child subagent" })
|
||||
const parent = await sessions.create({ title: "parent" })
|
||||
const child = await sessions.create({ parentID: parent.id, title: "child subagent" })
|
||||
|
||||
// Add a user message to the child so it has content
|
||||
const childMsgId = await userMsg(child.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: childMsgId,
|
||||
sessionID: child.id,
|
||||
@@ -90,7 +104,7 @@ describe("Session.fork child session remapping", () => {
|
||||
|
||||
// Add a user message then an assistant message with a task tool part referencing the child
|
||||
const parentUserMsg = await userMsg(parent.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: parentUserMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -99,7 +113,7 @@ describe("Session.fork child session remapping", () => {
|
||||
} as MessageV2.TextPart)
|
||||
|
||||
const parentAsstMsg = await asstMsg(parent.id, parentUserMsg)
|
||||
await Session.updatePart(
|
||||
await sessions.updatePart(
|
||||
taskPart({
|
||||
messageID: parentAsstMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -112,7 +126,7 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(forked.id).not.toBe(parent.id)
|
||||
|
||||
// Check that the forked session's task part references a DIFFERENT child session
|
||||
const forkedMsgs = await Session.messages({ sessionID: forked.id })
|
||||
const forkedMsgs = await sessions.messages({ sessionID: forked.id })
|
||||
const parts = forkedMsgs.flatMap((m) => m.parts)
|
||||
const tools = parts.filter((p) => p.type === "tool" && p.tool === "task") as MessageV2.ToolPart[]
|
||||
|
||||
@@ -121,11 +135,11 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(meta.sessionId).not.toBe(child.id)
|
||||
|
||||
// Verify the forked child session actually exists and has content
|
||||
const forkedChild = await Session.get(SessionID.make(meta.sessionId))
|
||||
const forkedChild = await sessions.get(SessionID.make(meta.sessionId))
|
||||
expect(forkedChild).toBeDefined()
|
||||
expect(forkedChild.id).not.toBe(child.id)
|
||||
|
||||
const forkedChildMsgs = await Session.messages({ sessionID: forkedChild.id })
|
||||
const forkedChildMsgs = await sessions.messages({ sessionID: forkedChild.id })
|
||||
expect(forkedChildMsgs).toHaveLength(1)
|
||||
expect(forkedChildMsgs[0].parts[0].type).toBe("text")
|
||||
},
|
||||
@@ -142,13 +156,13 @@ describe("Session.fork child session remapping", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
// grandchild -> child -> parent
|
||||
const parent = await Session.create({ title: "parent" })
|
||||
const child = await Session.create({ parentID: parent.id, title: "child" })
|
||||
const grandchild = await Session.create({ parentID: child.id, title: "grandchild" })
|
||||
const parent = await sessions.create({ title: "parent" })
|
||||
const child = await sessions.create({ parentID: parent.id, title: "child" })
|
||||
const grandchild = await sessions.create({ parentID: child.id, title: "grandchild" })
|
||||
|
||||
// grandchild has a text message
|
||||
const gcMsgId = await userMsg(grandchild.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: gcMsgId,
|
||||
sessionID: grandchild.id,
|
||||
@@ -158,7 +172,7 @@ describe("Session.fork child session remapping", () => {
|
||||
|
||||
// child references grandchild via task part
|
||||
const childUserMsg = await userMsg(child.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: childUserMsg,
|
||||
sessionID: child.id,
|
||||
@@ -166,7 +180,7 @@ describe("Session.fork child session remapping", () => {
|
||||
text: "question",
|
||||
} as MessageV2.TextPart)
|
||||
const childAsstMsg = await asstMsg(child.id, childUserMsg)
|
||||
await Session.updatePart(
|
||||
await sessions.updatePart(
|
||||
taskPart({
|
||||
messageID: childAsstMsg,
|
||||
sessionID: child.id,
|
||||
@@ -176,7 +190,7 @@ describe("Session.fork child session remapping", () => {
|
||||
|
||||
// parent references child via task part
|
||||
const parentUserMsg = await userMsg(parent.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: parentUserMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -184,7 +198,7 @@ describe("Session.fork child session remapping", () => {
|
||||
text: "request",
|
||||
} as MessageV2.TextPart)
|
||||
const parentAsstMsg = await asstMsg(parent.id, parentUserMsg)
|
||||
await Session.updatePart(
|
||||
await sessions.updatePart(
|
||||
taskPart({
|
||||
messageID: parentAsstMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -195,7 +209,7 @@ describe("Session.fork child session remapping", () => {
|
||||
const forked = await Session.fork({ sessionID: parent.id })
|
||||
|
||||
// Verify parent-level remap
|
||||
const forkedMsgs = await Session.messages({ sessionID: forked.id })
|
||||
const forkedMsgs = await sessions.messages({ sessionID: forked.id })
|
||||
const tools = forkedMsgs
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "tool" && p.tool === "task") as MessageV2.ToolPart[]
|
||||
@@ -203,7 +217,7 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(forkedChildID).not.toBe(child.id)
|
||||
|
||||
// Verify child-level remap (grandchild)
|
||||
const forkedChildMsgs = await Session.messages({ sessionID: SessionID.make(forkedChildID) })
|
||||
const forkedChildMsgs = await sessions.messages({ sessionID: SessionID.make(forkedChildID) })
|
||||
const childTools = forkedChildMsgs
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "tool" && p.tool === "task") as MessageV2.ToolPart[]
|
||||
@@ -213,7 +227,7 @@ describe("Session.fork child session remapping", () => {
|
||||
expect(forkedGrandchildID).not.toBe(grandchild.id)
|
||||
|
||||
// Verify grandchild content was copied
|
||||
const gcMsgs = await Session.messages({ sessionID: SessionID.make(forkedGrandchildID) })
|
||||
const gcMsgs = await sessions.messages({ sessionID: SessionID.make(forkedGrandchildID) })
|
||||
expect(gcMsgs).toHaveLength(1)
|
||||
},
|
||||
})
|
||||
@@ -228,9 +242,9 @@ describe("Session.fork child session remapping", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const parent = await Session.create({ title: "parent" })
|
||||
const parent = await sessions.create({ title: "parent" })
|
||||
const parentUserMsg = await userMsg(parent.id)
|
||||
await Session.updatePart({
|
||||
await sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: parentUserMsg,
|
||||
sessionID: parent.id,
|
||||
@@ -239,7 +253,7 @@ describe("Session.fork child session remapping", () => {
|
||||
} as MessageV2.TextPart)
|
||||
|
||||
const forked = await Session.fork({ sessionID: parent.id })
|
||||
const forkedMsgs = await Session.messages({ sessionID: forked.id })
|
||||
const forkedMsgs = await sessions.messages({ sessionID: forked.id })
|
||||
expect(forkedMsgs).toHaveLength(1)
|
||||
expect(forkedMsgs[0].parts[0].type).toBe("text")
|
||||
expect((forkedMsgs[0].parts[0] as MessageV2.TextPart).text).toBe("hello")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { ProjectTable } from "../../src/project/project.sql"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionTable } from "../../src/session/session.sql"
|
||||
import { Database, eq } from "../../src/storage/db"
|
||||
@@ -21,7 +21,9 @@ describe("Kilo Session.list", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({ title: "legacy-session" })
|
||||
const session = await Effect.runPromise(
|
||||
Session.Service.use((svc) => svc.create({ title: "legacy-session" })).pipe(Effect.provide(Session.defaultLayer)),
|
||||
)
|
||||
const project = ProjectID.make("legacy-project")
|
||||
Database.use((db) => {
|
||||
db.insert(ProjectTable)
|
||||
@@ -37,7 +39,9 @@ describe("Kilo Session.list", () => {
|
||||
db.update(SessionTable).set({ project_id: project }).where(eq(SessionTable.id, session.id)).run()
|
||||
})
|
||||
|
||||
const sessions = await AppRuntime.runPromise(Session.Service.use((svc) => svc.list({ directory: tmp.path })))
|
||||
const sessions = await Effect.runPromise(
|
||||
Session.Service.use((svc) => svc.list({ directory: tmp.path })).pipe(Effect.provide(Session.defaultLayer)),
|
||||
)
|
||||
const ids = sessions.map((item) => item.id)
|
||||
|
||||
expect(ids).toContain(session.id)
|
||||
|
||||
@@ -17,6 +17,15 @@ import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const sessions = {
|
||||
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
updateMessage: <T extends MessageV2.Info>(msg: T) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
|
||||
}
|
||||
|
||||
function line(input: unknown) {
|
||||
return `data: ${JSON.stringify(input)}\n\n`
|
||||
}
|
||||
@@ -259,14 +268,14 @@ describe("session prompt queue", () => {
|
||||
await WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({ title: "Queued compaction regression" })
|
||||
const session = await sessions.create({ title: "Queued compaction regression" })
|
||||
const first = MessageID.ascending()
|
||||
const ans = MessageID.ascending()
|
||||
const queued = MessageID.ascending()
|
||||
|
||||
await Session.updateMessage(user(session.id, first).info)
|
||||
await Session.updateMessage(assistant(session.id, ans, first).info)
|
||||
await Session.updateMessage(user(session.id, queued).info)
|
||||
await sessions.updateMessage(user(session.id, first).info)
|
||||
await sessions.updateMessage(assistant(session.id, ans, first).info)
|
||||
await sessions.updateMessage(user(session.id, queued).info)
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
KiloSessionPromptQueue.enqueue(
|
||||
@@ -280,7 +289,7 @@ describe("session prompt queue", () => {
|
||||
auto: true,
|
||||
overflow: true,
|
||||
})
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
const messages = await sessions.messages({ sessionID: session.id })
|
||||
const compact = messages.find((msg) => msg.parts.some((part) => part.type === "compaction"))?.info.id
|
||||
return { compact, ids: KiloSessionPromptQueue.scope(session.id, messages).map((item) => item.info.id) }
|
||||
}),
|
||||
@@ -426,7 +435,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Queued prompt regression" })
|
||||
const session = await sessions.create({ title: "Queued prompt regression" })
|
||||
const first = Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
@@ -457,7 +466,7 @@ describe("session prompt queue", () => {
|
||||
expect(hasText(one, "first reply")).toBe(true)
|
||||
expect(hasText(two, "second reply")).toBe(true)
|
||||
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await sessions.messages({ sessionID: session.id })
|
||||
const users = msgs.filter((msg) => msg.info.role === "user")
|
||||
const assistants = msgs.filter((msg) => msg.info.role === "assistant")
|
||||
const prompts = users.flatMap((msg) =>
|
||||
@@ -548,7 +557,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Queued cancel regression" })
|
||||
const session = await sessions.create({ title: "Queued cancel regression" })
|
||||
const first = Effect.runPromise(
|
||||
prompt.prompt({
|
||||
sessionID: session.id,
|
||||
@@ -582,7 +591,7 @@ describe("session prompt queue", () => {
|
||||
|
||||
// The queued prompts must never reach the LLM once cancel flushes the queue.
|
||||
expect(calls).toHaveLength(1)
|
||||
const msgs = await Session.messages({ sessionID: session.id })
|
||||
const msgs = await sessions.messages({ sessionID: session.id })
|
||||
const assistants = msgs.filter((msg) => msg.info.role === "assistant")
|
||||
expect(assistants).toHaveLength(1)
|
||||
expect(msgs.filter((msg) => msg.info.role === "user")).toHaveLength(3)
|
||||
@@ -614,7 +623,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Suggestion unblock regression" })
|
||||
const session = await sessions.create({ title: "Suggestion unblock regression" })
|
||||
const offShown = Bus.subscribe(Suggestion.Event.Shown, (event) => {
|
||||
if (event.properties.sessionID === session.id) shown.resolve()
|
||||
})
|
||||
@@ -662,7 +671,7 @@ describe("session prompt queue", () => {
|
||||
directory: tmp.path,
|
||||
fn: async () =>
|
||||
scoped(tmp.path, async (prompt) => {
|
||||
const session = await Session.create({ title: "Question unblock regression" })
|
||||
const session = await sessions.create({ title: "Question unblock regression" })
|
||||
const offAsked = Bus.subscribe(Question.Event.Asked, (event) => {
|
||||
if (event.properties.sessionID === session.id) asked.resolve()
|
||||
})
|
||||
|
||||
@@ -25,8 +25,10 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
function run<A>(body: (snapshot: Snapshot.Interface) => Effect.Effect<A>) {
|
||||
return Effect.runPromise(Snapshot.Service.use(body).pipe(Effect.provide(Snapshot.defaultLayer)))
|
||||
function run<A>(body: (snapshot: Snapshot.Interface) => Effect.Effect<A, never, Session.Service>) {
|
||||
return Effect.runPromise(
|
||||
Snapshot.Service.use(body).pipe(Effect.provide(Snapshot.defaultLayer), Effect.provide(Session.defaultLayer)),
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -55,7 +57,8 @@ test("pathological diffFull workload finishes quickly and does not block abort",
|
||||
fn: () =>
|
||||
run((snapshot) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Effect.promise(() => Session.create({}))
|
||||
const sessions = yield* Session.Service
|
||||
const session = yield* sessions.create({})
|
||||
|
||||
const before = yield* snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
@@ -7,6 +7,8 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { RemoteSender } from "../../src/kilo-sessions/remote-sender"
|
||||
import { Effect } from "effect"
|
||||
import { Session } from "../../src/session/session"
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(RemoteSender, "create").mockReturnValue({ handle() {}, dispose() {} })
|
||||
@@ -19,6 +21,9 @@ afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
const create = (title: string) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create({ title })).pipe(Effect.provide(Session.defaultLayer)))
|
||||
|
||||
describe("experimental.session.list", () => {
|
||||
test("filters sessions by repo worktree family even when project IDs drift", async () => {
|
||||
await using first = await tmpdir({ git: true })
|
||||
@@ -30,12 +35,11 @@ describe("experimental.session.list", () => {
|
||||
|
||||
try {
|
||||
const { Server } = await import("../../src/server/server")
|
||||
const { Session } = await import("../../src/session/session")
|
||||
|
||||
// Create worktree session first so it computes its own project ID via rev-list
|
||||
const branch = await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "worktree-session" }),
|
||||
fn: () => create("worktree-session"),
|
||||
})
|
||||
|
||||
// Now write a stale project ID to .git/kilo — this overrides the root's cached ID
|
||||
@@ -48,14 +52,14 @@ describe("experimental.session.list", () => {
|
||||
project: await Server.Default().app.request("/project/current", {
|
||||
headers: { "x-kilo-directory": first.path },
|
||||
}),
|
||||
session: await Session.create({ title: "root-session" }),
|
||||
session: await create("root-session"),
|
||||
}),
|
||||
})
|
||||
await Bun.file(path.join(first.path, ".git", "kilo")).delete()
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "other-project-session" }),
|
||||
fn: () => create("other-project-session"),
|
||||
})
|
||||
|
||||
const app = root.app
|
||||
@@ -96,11 +100,10 @@ describe("experimental.session.list", () => {
|
||||
|
||||
try {
|
||||
const { Server } = await import("../../src/server/server")
|
||||
const { Session } = await import("../../src/session/session")
|
||||
|
||||
const branch = await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "worktree-session" }),
|
||||
fn: () => create("worktree-session"),
|
||||
})
|
||||
|
||||
const root = await WithInstance.provide({
|
||||
@@ -110,13 +113,13 @@ describe("experimental.session.list", () => {
|
||||
project: await Server.Default().app.request("/project/current", {
|
||||
headers: { "x-kilo-directory": first.path },
|
||||
}),
|
||||
session: await Session.create({ title: "root-session" }),
|
||||
session: await create("root-session"),
|
||||
}),
|
||||
})
|
||||
|
||||
await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "other-project-session" }),
|
||||
fn: () => create("other-project-session"),
|
||||
})
|
||||
|
||||
const app = root.app
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { $ } from "bun"
|
||||
import { Effect } from "effect"
|
||||
import { Session } from "../../src/session/session"
|
||||
import path from "path"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { RecallTool } from "../../src/tool/recall"
|
||||
@@ -32,6 +33,9 @@ afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
const create = (title: string) =>
|
||||
Effect.runPromise(Session.Service.use((svc) => svc.create({ title })).pipe(Effect.provide(Session.defaultLayer)))
|
||||
|
||||
describe("tool.recall", () => {
|
||||
test("search is limited to the current project worktrees", async () => {
|
||||
await using first = await tmpdir({ git: true })
|
||||
@@ -43,18 +47,17 @@ describe("tool.recall", () => {
|
||||
await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
|
||||
|
||||
try {
|
||||
const { Session } = await import("../../src/session/session")
|
||||
await WithInstance.provide({
|
||||
directory: first.path,
|
||||
fn: async () => Session.create({ title: "search-target root" }),
|
||||
fn: () => create("search-target root"),
|
||||
})
|
||||
await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "search-target worktree" }),
|
||||
fn: () => create("search-target worktree"),
|
||||
})
|
||||
await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "search-target other" }),
|
||||
fn: () => create("search-target other"),
|
||||
})
|
||||
|
||||
const result = await WithInstance.provide({
|
||||
@@ -82,10 +85,9 @@ describe("tool.recall", () => {
|
||||
await using second = await tmpdir({ git: true })
|
||||
|
||||
try {
|
||||
const { Session } = await import("../../src/session/session")
|
||||
const session = await WithInstance.provide({
|
||||
directory: second.path,
|
||||
fn: async () => Session.create({ title: "other-project-session" }),
|
||||
fn: () => create("other-project-session"),
|
||||
})
|
||||
|
||||
const err = await WithInstance.provide({
|
||||
@@ -115,10 +117,9 @@ describe("tool.recall", () => {
|
||||
await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
|
||||
|
||||
try {
|
||||
const { Session } = await import("../../src/session/session")
|
||||
const session = await WithInstance.provide({
|
||||
directory: worktree,
|
||||
fn: async () => Session.create({ title: "worktree readable" }),
|
||||
fn: () => create("worktree readable"),
|
||||
})
|
||||
|
||||
const result = await WithInstance.provide({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/plugin",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@kilocode/sdk",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"dependencies": {},
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -25,7 +25,6 @@ const allow: Record<string, string> = {
|
||||
"installation/index.ts": "existing installation facade outside #10655",
|
||||
"question/index.ts": "transitional facade deferred for upstream reconciliation in #10655",
|
||||
"session/compaction.ts": "existing compaction facade outside #10655",
|
||||
"session/session.ts": "transitional facade tracked by #10655",
|
||||
"sync/index.ts": "sync event runtime boundary",
|
||||
}
|
||||
|
||||
@@ -37,7 +36,6 @@ const testAllow: Record<string, { count: number; reason: string }> = {
|
||||
"kilocode/config-resilience.test.ts": { count: 4, reason: "existing runtime integration test" },
|
||||
"kilocode/config-validation.test.ts": { count: 2, reason: "existing runtime integration test" },
|
||||
"kilocode/plan-followup.test.ts": { count: 7, reason: "existing runtime integration test" },
|
||||
"kilocode/session-list.test.ts": { count: 2, reason: "existing runtime integration test" },
|
||||
"kilocode/session/platform-attribution.test.ts": { count: 5, reason: "existing runtime integration test" },
|
||||
"kilocode/session/session.test.ts": { count: 4, reason: "existing runtime integration test" },
|
||||
"mcp/headers.test.ts": { count: 4, reason: "existing runtime integration test" },
|
||||
@@ -86,7 +84,9 @@ const testInvalid = testHits.filter((hit) => !testAllow[hit.file])
|
||||
const testDrift = Object.entries(testAllow).flatMap(([file, entry]) => {
|
||||
const count = testHits.filter((hit) => hit.file === file).length
|
||||
if (count === entry.count) return []
|
||||
return [` packages/opencode/test/${file}: expected ${entry.count} classified reference(s), found ${count} (${entry.reason})`]
|
||||
return [
|
||||
` packages/opencode/test/${file}: expected ${entry.count} classified reference(s), found ${count} (${entry.reason})`,
|
||||
]
|
||||
})
|
||||
|
||||
if (invalid.length > 0 || drift.length > 0 || testInvalid.length > 0 || testDrift.length > 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/upstream-merge",
|
||||
"version": "7.3.16",
|
||||
"version": "7.3.17",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Scripts for automating upstream opencode merges into Kilo",
|
||||
|
||||
Reference in New Issue
Block a user