diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 4927d179d3..a944441c45 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -25,7 +25,7 @@ import { TodoWriteTool } from "../../tool/todo" import { Locale } from "@/util/locale" import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" // kilocode_change import { KiloRunAuto } from "@/kilocode/cli/run-auto" // kilocode_change -import { DaemonClient } from "@/kilocode/daemon/client" // kilocode_change +import { KiloRunDaemon } from "@/kilocode/cli/cmd/run" // kilocode_change import { Effect } from "effect" import { effectCmd } from "../effect-cmd" import { ServerAuth } from "@/server/auth" @@ -746,14 +746,7 @@ export const RunCommand = effectCmd({ return await execute(sdk) } - // kilocode_change start - default local runs attach to the daemon unless explicitly disabled - const daemon = await DaemonClient.maybe(DaemonClient.options()) - if (daemon) { - const dir = directory ?? Filesystem.resolve(process.cwd()) - const sdk = createKiloClient({ baseUrl: daemon.url, directory: dir, headers: daemon.headers }) - return await execute(sdk) - } - // kilocode_change end + if (await KiloRunDaemon.attach({ directory, execute })) return // kilocode_change const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 996ac084e9..2a3e093e59 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -17,7 +17,7 @@ import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" import { createKiloClient } from "@kilocode/sdk/v2" // kilocode_change import { writeHeapSnapshot } from "v8" import { TuiConfig } from "./config/tui" -import { DaemonClient } from "@/kilocode/daemon/client" // kilocode_change +import { KiloTuiThreadDaemon } from "@/kilocode/cli/cmd/tui/thread" // kilocode_change import { KILO_PROCESS_ROLE, KILO_RUN_ID, @@ -171,59 +171,7 @@ export const TuiThreadCommand = cmd({ } const cwd = Filesystem.resolve(process.cwd()) // kilocode_change start - default TUI sessions attach to the daemon unless explicitly disabled - const net = resolveNetworkOptionsNoConfig(args) - const daemon = await DaemonClient.maybe(DaemonClient.options(net)) - if (daemon) { - const prompt = await input(args.prompt) - const config = await TuiConfig.get() - - try { - await validateSession({ - url: daemon.url, - sessionID: args.session, - directory: cwd, - headers: daemon.headers, - }) - } catch (error) { - UI.error(errorMessage(error)) - process.exitCode = 1 - return - } - - if (args.cloudFork && args.session) { - UI.println("Importing session from cloud...") - const sdk = createKiloClient({ - baseUrl: daemon.url, - directory: cwd, - headers: daemon.headers, - }) - const id = await importCloudSession(sdk, args.session).catch(() => undefined) - if (!id) { - UI.error("Failed to import session from cloud") - process.exitCode = 1 - return - } - args.session = id - args.cloudFork = false - } - - await start({ - // kilocode_change - url: daemon.url, - config, - directory: cwd, - headers: daemon.headers, - args: { - continue: args.continue, - sessionID: args.session, - agent: args.agent, - model: args.model, - prompt, - fork: args.fork, - }, - }) - return - } + if (await KiloTuiThreadDaemon.attach({ args, cwd, input: () => input(args.prompt), start })) return // kilocode_change end const env = sanitizedProcessEnv({ [KILO_PROCESS_ROLE]: "worker", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 4b53c6742e..e4038684b0 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -570,15 +570,15 @@ export const layer = Layer.effect( Duration.infinity, ) + // kilocode_change start - detect global config edits made by other Kilo processes const refreshGlobal = Effect.fnUntraced(function* () { - // kilocode_change start - detect global config edits made by other Kilo processes const stamp = yield* KilocodeGlobalConfigStamp.read(fs, Global.Path.config) if (!globalStamp || stamp === globalStamp) return false globalStamp = stamp yield* invalidateGlobal return true - // kilocode_change end }) + // kilocode_change end const getGlobal = Effect.fn("Config.getGlobal")(function* () { yield* refreshGlobal() // kilocode_change @@ -1000,9 +1000,11 @@ export const layer = Layer.effect( ) const get = Effect.fn("Config.get")(function* () { + // kilocode_change start - reload instance config when global config changed elsewhere if (yield* refreshGlobal()) { yield* InstanceState.invalidate(state).pipe(Effect.catchCause(() => Effect.void)) } + // kilocode_change end return yield* InstanceState.use(state, (s) => s.config) }) @@ -1098,9 +1100,9 @@ export const layer = Layer.effect( } // kilocode_change end + if (changed) yield* invalidate() + // kilocode_change start - hot-reload global config changes in the active instance if (changed) { - yield* invalidate() - // kilocode_change start - hot-reload global config changes in the active instance yield* InstanceState.invalidate(state).pipe(Effect.catchCause(() => Effect.void)) yield* Effect.sync(() => GlobalBus.emit("event", { @@ -1111,8 +1113,8 @@ export const layer = Layer.effect( }, }), ).pipe(Effect.catchCause(() => Effect.void)) - // kilocode_change end } + // kilocode_change end return { info: next, changed } }) diff --git a/packages/opencode/src/kilocode/cli/cmd/run.ts b/packages/opencode/src/kilocode/cli/cmd/run.ts new file mode 100644 index 0000000000..9ec78edc07 --- /dev/null +++ b/packages/opencode/src/kilocode/cli/cmd/run.ts @@ -0,0 +1,19 @@ +import { createKiloClient, type KiloClient } from "@kilocode/sdk/v2" +import { Filesystem } from "@/util/filesystem" +import { DaemonClient } from "@/kilocode/daemon/client" + +export namespace KiloRunDaemon { + export type Input = { + directory?: string + execute: (client: KiloClient) => Promise + } + + export async function attach(input: Input) { + const daemon = await DaemonClient.maybe(DaemonClient.options()) + if (!daemon) return false + const dir = input.directory ?? Filesystem.resolve(process.cwd()) + const client = createKiloClient({ baseUrl: daemon.url, directory: dir, headers: daemon.headers }) + await input.execute(client) + return true + } +} diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/thread.ts b/packages/opencode/src/kilocode/cli/cmd/tui/thread.ts new file mode 100644 index 0000000000..477af503f4 --- /dev/null +++ b/packages/opencode/src/kilocode/cli/cmd/tui/thread.ts @@ -0,0 +1,87 @@ +import { UI } from "@/cli/ui" +import { resolveNetworkOptionsNoConfig, type NetworkOptions } from "@/cli/network" +import { errorMessage } from "@/util/error" +import { TuiConfig } from "@/cli/cmd/tui/config/tui" +import { validateSession } from "@/cli/cmd/tui/validate-session" +import { importCloudSession } from "@/kilocode/cloud-session" +import { DaemonClient } from "@/kilocode/daemon/client" +import { createKiloClient } from "@kilocode/sdk/v2" + +type TuiInput = Parameters[0] + +type Args = NetworkOptions & { + prompt?: string + session?: string + cloudFork?: boolean + continue?: boolean + agent?: string + model?: string + fork?: boolean +} + +type Input = { + args: Args + cwd: string + input: () => Promise + start: (input: TuiInput) => Promise +} + +async function session(input: Input, daemon: DaemonClient.Connection) { + if (!input.args.cloudFork || !input.args.session) return { ok: true as const, id: input.args.session } + + UI.println("Importing session from cloud...") + const client = createKiloClient({ + baseUrl: daemon.url, + directory: input.cwd, + headers: daemon.headers, + }) + const id = await importCloudSession(client, input.args.session).catch(() => undefined) + if (id) return { ok: true as const, id } + + UI.error("Failed to import session from cloud") + process.exitCode = 1 + return { ok: false as const } +} + +export namespace KiloTuiThreadDaemon { + export async function attach(input: Input) { + const net = resolveNetworkOptionsNoConfig(input.args) + const daemon = await DaemonClient.maybe(DaemonClient.options(net)) + if (!daemon) return false + + const prompt = await input.input() + const config = await TuiConfig.get() + + try { + await validateSession({ + url: daemon.url, + sessionID: input.args.session, + directory: input.cwd, + headers: daemon.headers, + }) + } catch (error) { + UI.error(errorMessage(error)) + process.exitCode = 1 + return true + } + + const fork = await session(input, daemon) + if (!fork.ok) return true + + await input.start({ + url: daemon.url, + config, + directory: input.cwd, + headers: daemon.headers, + args: { + continue: input.args.continue, + sessionID: fork.id, + agent: input.args.agent, + model: input.args.model, + prompt, + fork: input.args.fork, + }, + }) + return true + } +} diff --git a/packages/opencode/src/kilocode/session/prompt.ts b/packages/opencode/src/kilocode/session/prompt.ts index 4980342079..8d388da92b 100644 --- a/packages/opencode/src/kilocode/session/prompt.ts +++ b/packages/opencode/src/kilocode/session/prompt.ts @@ -6,6 +6,7 @@ import { Cause, Effect, Exit } from "effect" import { SessionID, PartID } from "@/session/schema" import { MessageV2 } from "@/session/message-v2" import { Session } from "@/session/session" +import { Agent } from "@/agent/agent" import { Instance } from "@/project/instance" import type { SessionStatus } from "@/session/status" import { Flag } from "@opencode-ai/core/flag/flag" @@ -120,6 +121,23 @@ export namespace KiloSessionPrompt { return input.agent.permission } + export const askPermission = Effect.fn("KiloSessionPrompt.askPermission")(function* (input: { + permission: Pick + agents: Pick + sessions: Pick + agent: Agent.Info + session: Session.Info + request: Omit + }) { + const agent = (yield* input.agents.get(input.agent.name)) ?? input.agent + const session = yield* input.sessions.get(input.session.id).pipe(Effect.catchCause(() => Effect.succeed(input.session))) + yield* input.permission.ask({ + ...input.request, + ruleset: Permission.merge(agent.permission, guardPermissions({ agent, session })), + hardRuleset: hardPermissions({ agent }), + }) + }) + /** * Mutable cache for environment details, keyed by user message ID * so it recomputes when a new user message arrives. diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 7bc2588a2e..766d5eb8f7 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -179,16 +179,14 @@ export const layer = Layer.effect( const create = Effect.fn("Pty.create")(function* (input: CreateInput) { const s = yield* InstanceState.get(state) - // kilocode_change start const bridge = yield* EffectBridge.make() const cfg = yield* config.get() const id = PtyID.ascending() - // kilocode_change end const resolved = KiloPtySelfCommand.resolve(input) // kilocode_change const command = resolved.command || Shell.preferred(cfg.shell) // kilocode_change const args = resolved.args || [] // kilocode_change if (Shell.login(command)) { - args.push("-l") // kilocode_change + args.push("-l") } const cwd = resolved.cwd || s.dir // kilocode_change diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d76d6166f6..86bac0cb48 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -378,17 +378,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the return input.messages }) - // kilocode_change start - resolve permissions at ask time so active tools see config edits - const rules = Effect.fnUntraced(function* (input: { agent: Agent.Info; session: Session.Info }) { - const agent = (yield* agents.get(input.agent.name)) ?? input.agent - const session = yield* sessions.get(input.session.id).pipe(Effect.catchCause(() => Effect.succeed(input.session))) - return { - ruleset: Permission.merge(agent.permission, KiloSessionPrompt.guardPermissions({ agent, session })), - hardRuleset: KiloSessionPrompt.hardPermissions({ agent }), - } - }) - // kilocode_change end - const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: { agent: Agent.Info model: Provider.Model @@ -425,16 +414,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, } }), + // kilocode_change start - resolve permissions at ask time so active tools see config edits ask: (req) => - Effect.gen(function* () { - const current = yield* rules({ agent: input.agent, session: input.session }) - yield* permission.ask({ + KiloSessionPrompt.askPermission({ + permission, + agents, + sessions, + agent: input.agent, + session: input.session, + request: { ...req, sessionID: input.session.id, tool: { messageID: input.processor.message.id, callID: options.toolCallId }, - ...current, // kilocode_change - live permission rules - }) + }, }).pipe(Effect.orDie), + // kilocode_change end }) for (const item of yield* registry.tools({ @@ -662,15 +656,20 @@ NOTE: At any point in time through this workflow you should feel free to ask the state: { ...part.state, ...val }, } satisfies MessageV2.ToolPart) }), + // kilocode_change start - resolve permissions at ask time so active tools see config edits ask: (req: any) => - Effect.gen(function* () { - const current = yield* rules({ agent: taskAgent, session }) - yield* permission.ask({ + KiloSessionPrompt.askPermission({ + permission, + agents, + sessions, + agent: taskAgent, + session, + request: { ...req, sessionID, - ...current, // kilocode_change - live permission rules - }) + }, }).pipe(Effect.orDie), + // kilocode_change end }) .pipe( Effect.catchCause((cause) => { diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts index e951c3f603..53b7488c26 100644 --- a/packages/opencode/test/cli/tui/thread.test.ts +++ b/packages/opencode/test/cli/tui/thread.test.ts @@ -25,14 +25,4 @@ describe("tui thread", () => { test("uses the real cwd after resolving a relative project from PWD", async () => { await check(".") }) - - // kilocode_change start - test("ignores stale PWD after cwd is changed by a process wrapper", async () => { - await using root = await tmpdir() - const pkg = path.join(root.path, "packages", "opencode") - await fs.mkdir(pkg, { recursive: true }) - - expect(resolveThreadDirectory(".", root.path, pkg)).toBe(pkg) - }) - // kilocode_change end }) diff --git a/packages/opencode/test/kilocode/cli/tui/thread.test.ts b/packages/opencode/test/kilocode/cli/tui/thread.test.ts new file mode 100644 index 0000000000..5cb6e89fe3 --- /dev/null +++ b/packages/opencode/test/kilocode/cli/tui/thread.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "bun:test" +import fs from "fs/promises" +import path from "path" +import { tmpdir } from "../../../fixture/fixture" +import { resolveThreadDirectory } from "../../../../src/cli/cmd/tui/thread" + +describe("kilo tui thread", () => { + test("ignores stale PWD after cwd is changed by a process wrapper", async () => { + await using root = await tmpdir() + const pkg = path.join(root.path, "packages", "opencode") + await fs.mkdir(pkg, { recursive: true }) + + expect(resolveThreadDirectory(".", root.path, pkg)).toBe(pkg) + }) +}) diff --git a/packages/opencode/test/kilocode/session-prompt-permission-refresh.test.ts b/packages/opencode/test/kilocode/session-prompt-permission-refresh.test.ts new file mode 100644 index 0000000000..39c6a14fe5 --- /dev/null +++ b/packages/opencode/test/kilocode/session-prompt-permission-refresh.test.ts @@ -0,0 +1,260 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { expect } from "bun:test" +import { Effect, Exit, Fiber, Layer } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import path from "path" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import * as Log from "@opencode-ai/core/util/log" +import { Agent as AgentSvc } from "../../src/agent/agent" +import { Bus } from "../../src/bus" +import { Command } from "../../src/command" +import { Config } from "../../src/config/config" +import { Env } from "../../src/env" +import { Format } from "../../src/format" +import { Git } from "../../src/git" +import { LSP } from "../../src/lsp/lsp" +import { MCP } from "../../src/mcp" +import { Permission } from "../../src/permission" +import { Plugin } from "../../src/plugin" +import { Provider as ProviderSvc } from "../../src/provider/provider" +import { Question } from "../../src/question" +import { SessionCompaction } from "../../src/session/compaction" +import { Instruction } from "../../src/session/instruction" +import { LLM } from "../../src/session/llm" +import { SessionProcessor } from "../../src/session/processor" +import { SessionPrompt } from "../../src/session/prompt" +import { SessionRevert } from "../../src/session/revert" +import { SessionRunState } from "../../src/session/run-state" +import { Session } from "../../src/session/session" +import { SessionStatus } from "../../src/session/status" +import { SystemPrompt } from "../../src/session/system" +import { SessionSummary } from "../../src/session/summary" +import { Todo } from "../../src/session/todo" +import { Skill } from "../../src/skill" +import { Snapshot } from "../../src/snapshot" +import { Ripgrep } from "../../src/file/ripgrep" +import { ToolRegistry } from "../../src/tool/registry" +import { Truncate } from "../../src/tool/truncate" +import { provideTmpdirServer } from "../fixture/fixture" +import { testEffect } from "../lib/effect" +import { reply, TestLLMServer } from "../lib/llm-server" + +void Log.init({ print: false }) + +const waitFor = (label: string, run: Effect.Effect) => + Effect.gen(function* () { + const end = Date.now() + 5_000 + while (Date.now() < end) { + const result = yield* run + if (result !== undefined) return result + yield* Effect.sleep(20) + } + throw new Error(`timed out waiting for ${label}`) + }) + +const summary = Layer.succeed( + SessionSummary.Service, + SessionSummary.Service.of({ + summarize: () => Effect.void, + diff: () => Effect.succeed([]), + computeDiff: () => Effect.succeed([]), + }), +) + +const mcp = Layer.succeed( + MCP.Service, + MCP.Service.of({ + status: () => Effect.succeed({}), + clients: () => Effect.succeed({}), + tools: () => Effect.succeed({}), + prompts: () => Effect.succeed({}), + resources: () => Effect.succeed({}), + add: () => Effect.succeed({ status: { status: "disabled" as const } }), + connect: () => Effect.void, + disconnect: () => Effect.void, + getPrompt: () => Effect.succeed(undefined), + readResource: () => Effect.succeed(undefined), + startAuth: () => Effect.die("unexpected MCP auth in permission refresh tests"), + authenticate: () => Effect.die("unexpected MCP auth in permission refresh tests"), + finishAuth: () => Effect.die("unexpected MCP auth in permission refresh tests"), + removeAuth: () => Effect.void, + supportsOAuth: () => Effect.succeed(false), + hasStoredTokens: () => Effect.succeed(false), + getAuthStatus: () => Effect.succeed("not_authenticated" as const), + }), +) + +const lsp = Layer.succeed( + LSP.Service, + LSP.Service.of({ + init: () => Effect.void, + status: () => Effect.succeed([]), + hasClients: () => Effect.succeed(false), + touchFile: () => Effect.void, + diagnostics: () => Effect.succeed({}), + hover: () => Effect.succeed(undefined), + definition: () => Effect.succeed([]), + references: () => Effect.succeed([]), + implementation: () => Effect.succeed([]), + documentSymbol: () => Effect.succeed([]), + workspaceSymbol: () => Effect.succeed([]), + prepareCallHierarchy: () => Effect.succeed([]), + incomingCalls: () => Effect.succeed([]), + outgoingCalls: () => Effect.succeed([]), + }), +) + +const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer)) +const run = SessionRunState.layer.pipe(Layer.provide(status)) +const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) + +function makeHttp() { + const deps = Layer.mergeAll( + Session.defaultLayer, + Snapshot.defaultLayer, + LLM.defaultLayer, + Env.defaultLayer, + AgentSvc.defaultLayer, + Command.defaultLayer, + Permission.defaultLayer, + Plugin.defaultLayer, + Config.defaultLayer, + ProviderSvc.defaultLayer, + lsp, + mcp, + AppFileSystem.defaultLayer, + status, + ).pipe(Layer.provideMerge(infra)) + const question = Question.layer.pipe(Layer.provideMerge(deps)) + const todo = Todo.layer.pipe(Layer.provideMerge(deps)) + const registry = ToolRegistry.layer.pipe( + Layer.provide(Skill.defaultLayer), + Layer.provide(FetchHttpClient.layer), + Layer.provide(CrossSpawnSpawner.defaultLayer), + Layer.provide(Ripgrep.defaultLayer), + Layer.provide(Format.defaultLayer), + Layer.provide(Git.defaultLayer), + Layer.provideMerge(todo), + Layer.provideMerge(question), + Layer.provideMerge(deps), + ) + const trunc = Truncate.layer.pipe(Layer.provideMerge(deps)) + const proc = SessionProcessor.layer.pipe(Layer.provide(summary), Layer.provideMerge(deps)) + const compact = SessionCompaction.layer.pipe(Layer.provideMerge(proc), Layer.provideMerge(deps)) + return Layer.mergeAll( + TestLLMServer.layer, + SessionPrompt.layer.pipe( + Layer.provide(SessionRevert.defaultLayer), + Layer.provide(summary), + Layer.provideMerge(run), + Layer.provideMerge(compact), + Layer.provideMerge(proc), + Layer.provideMerge(registry), + Layer.provideMerge(trunc), + Layer.provide(Instruction.defaultLayer), + Layer.provide(SystemPrompt.defaultLayer), + Layer.provideMerge(deps), + ), + ).pipe(Layer.provide(summary)) +} + +const it = testEffect(makeHttp()) + +const cfg = { + provider: { + test: { + name: "Test", + id: "test", + env: [], + npm: "@ai-sdk/openai-compatible", + models: { + "test-model": { + id: "test-model", + name: "Test Model", + attachment: false, + reasoning: false, + temperature: false, + tool_call: true, + release_date: "2025-01-01", + limit: { context: 100000, output: 10000 }, + cost: { input: 0, output: 0 }, + options: {}, + }, + }, + options: { + apiKey: "test-key", + baseURL: "http://localhost:1/v1", + }, + }, + }, +} + +function providerCfg(url: string) { + return { + ...cfg, + provider: { + ...cfg.provider, + test: { + ...cfg.provider.test, + options: { + ...cfg.provider.test.options, + baseURL: url, + }, + }, + }, + } +} + +it.live("active tool calls use permissions changed after model streaming starts", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ dir, llm }) { + const config = yield* Config.Service + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const permission = yield* Permission.Service + const file = path.join(dir, "note.txt") + const gate = Promise.withResolvers() + + yield* Effect.promise(() => Bun.write(file, "old")) + yield* llm.push( + reply() + .wait(gate.promise) + .tool("edit", { filePath: file, oldString: "old", newString: "new" }), + ) + + const chat = yield* sessions.create({ title: "Pinned" }) + yield* prompt.prompt({ + sessionID: chat.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "edit note" }], + }) + + const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkScoped) + yield* llm.wait(1) + yield* config.update({ permission: { edit: { "*": "allow" } } } as Config.Info) + gate.resolve(undefined) + + yield* waitFor( + "edit without permission prompt", + Effect.gen(function* () { + const pending = yield* permission.list() + if (pending.length) throw new Error("edit permission was requested after config allowed it") + const text = yield* Effect.promise(() => Bun.file(file).text()) + if (text === "new") return text + }), + ) + + const exit = yield* Fiber.await(fiber) + expect(Exit.isSuccess(exit)).toBe(true) + }), + { + git: true, + config: (url) => ({ + ...providerCfg(url), + permission: { edit: "ask" }, + }), + }, + ), +) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 9ea156f408..bfa87d8f10 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -395,62 +395,6 @@ it.live("loop calls LLM and returns assistant message", () => { git: true, config: providerCfg }, ), ) - -// kilocode_change start - active tools must re-read permissions after config changes -it.live("active tool calls use permissions changed after model streaming starts", () => - provideTmpdirServer( - Effect.fnUntraced(function* ({ dir, llm }) { - const config = yield* Config.Service - const prompt = yield* SessionPrompt.Service - const sessions = yield* Session.Service - const permission = yield* Permission.Service - const file = path.join(dir, "note.txt") - const gate = defer() - - yield* Effect.promise(() => Bun.write(file, "old")) - yield* llm.push( - reply() - .wait(gate.promise) - .tool("edit", { filePath: file, oldString: "old", newString: "new" }), - ) - - const chat = yield* sessions.create({ title: "Pinned" }) - yield* prompt.prompt({ - sessionID: chat.id, - agent: "build", - noReply: true, - parts: [{ type: "text", text: "edit note" }], - }) - - const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkScoped) - yield* llm.wait(1) - yield* config.update({ permission: { edit: { "*": "allow" } } } as Config.Info) - gate.resolve(undefined) - - yield* waitFor( - "edit without permission prompt", - Effect.gen(function* () { - const pending = yield* permission.list() - if (pending.length) throw new Error("edit permission was requested after config allowed it") - const text = yield* Effect.promise(() => Bun.file(file).text()) - if (text === "new") return text - }), - ) - - const exit = yield* Fiber.await(fiber) - expect(Exit.isSuccess(exit)).toBe(true) - }), - { - git: true, - config: (url) => ({ - ...providerCfg(url), - permission: { edit: "ask" }, - }), - }, - ), -) -// kilocode_change end - it.live("prompt emits v2 prompted and synthetic events", () => provideTmpdirServer( Effect.fnUntraced(function* () {