mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 14:16:30 +00:00
Compare commits
2
Commits
location-path
..
v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ec0b454c | ||
|
|
f4baba2824 |
@@ -16,9 +16,8 @@ import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { installSseTransport } from "../utils/sse-transport"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const initialPageSize = 20
|
||||
const historyPageSize = 200
|
||||
const messages = Array.from({ length: initialPageSize + 1 }, (_, index) => {
|
||||
const messagePageSize = 200
|
||||
const messages = Array.from({ length: messagePageSize / 2 + 1 }, (_, index) => {
|
||||
const id = `msg_${String(index + 1001).padStart(4, "0")}_history_root_user`
|
||||
return [
|
||||
userMessage(undefined, { id, created: 1700000000000 + index * 2_000 }),
|
||||
@@ -26,7 +25,7 @@ const messages = Array.from({ length: initialPageSize + 1 }, (_, index) => {
|
||||
id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`,
|
||||
parentID: id,
|
||||
created: 1700000001000 + index * 2_000,
|
||||
completed: index < initialPageSize,
|
||||
completed: index < messagePageSize / 2,
|
||||
}),
|
||||
]
|
||||
}).flat()
|
||||
@@ -160,21 +159,18 @@ for (const scenario of scenarios) {
|
||||
await expect(page.locator(`[data-timeline-part-id="${userPartID}"]`)).toBeVisible()
|
||||
const viewport = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
|
||||
await viewport.hover()
|
||||
const deadline = Date.now() + 10_000
|
||||
const deadline = Date.now() + 30_000
|
||||
while (requests.filter((request) => request.phase === "start").length < 2) {
|
||||
if (Date.now() >= deadline) throw new Error("Timed out scrolling to the history boundary")
|
||||
await page.mouse.wheel(0, -240)
|
||||
await page.mouse.wheel(0, -1_200)
|
||||
await page.waitForTimeout(20)
|
||||
}
|
||||
expect(requests.filter((request) => request.phase === "end")).toHaveLength(1)
|
||||
expect(sequence.slice(0, 3)).toEqual([
|
||||
"messages:start:latest",
|
||||
"messages:end:latest",
|
||||
`messages:start:${messages.at(-initialPageSize)!.info.id}`,
|
||||
`messages:start:${messages.at(-messagePageSize)!.info.id}`,
|
||||
])
|
||||
await expect(page.locator('[data-timeline-part-id*="_history_root_assistant:text:0"]')).toHaveCount(
|
||||
initialPageSize / 2,
|
||||
)
|
||||
await page.evaluate(() => {
|
||||
;(
|
||||
window as Window & {
|
||||
@@ -186,15 +182,12 @@ for (const scenario of scenarios) {
|
||||
expect(await visibleContentHidden(page)).toBe(false)
|
||||
const beforeHistory = await probeSamples(page)
|
||||
history.resolve()
|
||||
await expect
|
||||
.poll(() => page.locator('[data-timeline-part-id*="_history_root_assistant:text:0"]').count())
|
||||
.toBeGreaterThan(initialPageSize / 2)
|
||||
await expect.poll(() => requests.filter((request) => request.phase === "end").length).toBe(2)
|
||||
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
|
||||
await waitForProbeSamples(page, beforeHistory)
|
||||
expect(pages).toEqual([
|
||||
{ before: undefined, limit: initialPageSize },
|
||||
{ before: messages.at(-initialPageSize)!.info.id, limit: historyPageSize },
|
||||
{ before: undefined, limit: messagePageSize },
|
||||
{ before: messages.at(-messagePageSize)!.info.id, limit: messagePageSize },
|
||||
])
|
||||
expect(roots).toEqual([])
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ function turn(index: number): Message[] {
|
||||
return [user, assistantMessage(targetID, index, user.info.id, parts)]
|
||||
}
|
||||
|
||||
const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat()
|
||||
const targetMessages = Array.from({ length: 101 }, (_, index) => turn(index)).flat()
|
||||
const sourceMessages = Array.from({ length: 12 }, (_, index) => [
|
||||
userMessage(sourceID, index + 1000, 120),
|
||||
assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]),
|
||||
|
||||
@@ -727,7 +727,7 @@ function expectCompleteScroll(
|
||||
).toEqual([])
|
||||
expect(new Set(expectedPartIDs).size).toBe(expectedPartIDs.length)
|
||||
expect(new Set(expectedMessageIDs).size).toBe(expectedMessageIDs.length)
|
||||
expect(expectedPartIDs.length).toBe(331)
|
||||
expect(expectedPartIDs.length).toBe(465)
|
||||
}
|
||||
|
||||
async function selectHomeProject(page: Page, projectName: string) {
|
||||
|
||||
@@ -618,7 +618,7 @@ describe("server session", () => {
|
||||
await ctx.store.sync("root")
|
||||
|
||||
expect(ctx.get).toEqual([{ sessionID: "root" }])
|
||||
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
|
||||
expect(ctx.messages).toEqual([{ sessionID: "root", limit: 200, order: "desc" }])
|
||||
expect(ctx.store.data.message.root).toEqual([])
|
||||
})
|
||||
|
||||
@@ -629,8 +629,32 @@ describe("server session", () => {
|
||||
ctx.store.invalidate()
|
||||
await ctx.store.sync("root")
|
||||
|
||||
expect(ctx.store.data.message.root).toEqual([])
|
||||
expect(ctx.get).toHaveLength(2)
|
||||
expect(ctx.messages).toHaveLength(2)
|
||||
expect(ctx.messages).toEqual([
|
||||
{ sessionID: "root", limit: 200, order: "desc" },
|
||||
{ sessionID: "root", limit: 200, order: "desc" },
|
||||
])
|
||||
})
|
||||
|
||||
test("keeps a fixed page size after the local message cache exceeds the API limit", async () => {
|
||||
const client = messageClient(response(), response())
|
||||
const store = createServerSession(client)
|
||||
await store.sync("child")
|
||||
Array.from({ length: 428 }, (_, index) =>
|
||||
store.apply({
|
||||
type: "message.updated",
|
||||
properties: { info: userMessage(`message-${index}`, { time: { created: index } }) },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(store.data.message.child).toHaveLength(428)
|
||||
await store.sync("child", { force: true })
|
||||
|
||||
expect(client.requests).toEqual([
|
||||
{ sessionID: "child", limit: 200, order: "desc" },
|
||||
{ sessionID: "child", limit: 200, order: "desc" },
|
||||
])
|
||||
})
|
||||
|
||||
test("loads current session content through the current message API", async () => {
|
||||
@@ -655,7 +679,7 @@ describe("server session", () => {
|
||||
|
||||
await store.sync("root")
|
||||
|
||||
expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
|
||||
expect(requests).toEqual([{ sessionID: "root", limit: 200, order: "desc" }])
|
||||
expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
|
||||
expect(store.data.message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
|
||||
})
|
||||
@@ -731,8 +755,8 @@ describe("server session", () => {
|
||||
await store.sync("root")
|
||||
|
||||
expect(requests).toEqual([
|
||||
{ sessionID: "root", limit: 20, order: "desc" },
|
||||
{ sessionID: "root", limit: 20, cursor: "older" },
|
||||
{ sessionID: "root", limit: 200, order: "desc" },
|
||||
{ sessionID: "root", limit: 200, cursor: "older" },
|
||||
])
|
||||
expect(store.data.message.root.map((message) => message.id)).toEqual([
|
||||
user.id,
|
||||
@@ -741,6 +765,26 @@ describe("server session", () => {
|
||||
expect(assistants.map((item) => store.data.part[item.id]?.[0]?.type)).toEqual(["text", "text", "text"])
|
||||
})
|
||||
|
||||
test("loads older messages by cursor with the fixed page size", async () => {
|
||||
const older = userMessage("message-1")
|
||||
const latest = userMessage("message-2", { time: { created: 2 } })
|
||||
const client = messageClient(
|
||||
response([{ info: latest, parts: [] }], "older"),
|
||||
response([{ info: older, parts: [] }]),
|
||||
)
|
||||
const store = createServerSession(client)
|
||||
await store.sync("child")
|
||||
|
||||
await store.history.loadMore("child")
|
||||
|
||||
expect(client.requests).toEqual([
|
||||
{ sessionID: "child", limit: 200, order: "desc" },
|
||||
{ sessionID: "child", limit: 200, cursor: "older" },
|
||||
])
|
||||
expect(store.data.message.child).toEqual([older, latest])
|
||||
expect(store.history.more("child")).toBe(false)
|
||||
})
|
||||
|
||||
// V2 messages are ordered projections and do not expose V1 assistant parent IDs.
|
||||
describe.skip("V1 assistant parent projections", () => {
|
||||
test("backfills an assistant-only initial page through its user root", async () => {
|
||||
@@ -754,7 +798,7 @@ describe("server session", () => {
|
||||
|
||||
await store.sync("child")
|
||||
|
||||
expect(client.requests).toEqual([{ sessionID: "child", limit: 20, order: "desc" }])
|
||||
expect(client.requests).toEqual([{ sessionID: "child", limit: 200, order: "desc" }])
|
||||
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
|
||||
expect(store.data.message.child).toEqual([user, ...assistants])
|
||||
expect(store.history.more("child")).toBe(false)
|
||||
|
||||
@@ -30,8 +30,7 @@ type MessageApi = ServerApi["message"]
|
||||
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const initialMessagePageSize = 20
|
||||
const historyMessagePageSize = 200
|
||||
const messagePageSize = 200
|
||||
const sessionInfoLimit = 2_048
|
||||
const emptyIDs: ReadonlySet<string> = new Set()
|
||||
|
||||
@@ -242,7 +241,6 @@ export function createServerSession(
|
||||
return created
|
||||
}
|
||||
const [meta, setMeta] = createStore({
|
||||
limit: {} as Record<string, number | undefined>,
|
||||
cursor: {} as Record<string, string | undefined>,
|
||||
complete: {} as Record<string, boolean | undefined>,
|
||||
loading: {} as Record<string, boolean | undefined>,
|
||||
@@ -423,7 +421,6 @@ export function createServerSession(
|
||||
setMeta(
|
||||
produce((draft) => {
|
||||
for (const sessionID of sessionIDs) {
|
||||
delete draft.limit[sessionID]
|
||||
delete draft.cursor[sessionID]
|
||||
delete draft.complete[sessionID]
|
||||
delete draft.loading[sessionID]
|
||||
@@ -457,11 +454,13 @@ export function createServerSession(
|
||||
pickSessionCacheEvictions({ seen, keep: sessionID, limit: SESSION_CACHE_LIMIT, preserve: protectedSessions() }),
|
||||
)
|
||||
|
||||
const fetchMessages = async (sessionID: string, limit: number, before?: string, onAttempt?: () => void) => {
|
||||
const fetchMessages = async (sessionID: string, before?: string, onAttempt?: () => void) => {
|
||||
const request = (cursor?: string) =>
|
||||
(options?.retry ?? retry)(() => {
|
||||
onAttempt?.()
|
||||
return messageApi.list(cursor ? { sessionID, limit, cursor } : { sessionID, limit, order: "desc" })
|
||||
return messageApi.list(
|
||||
cursor ? { sessionID, limit: messagePageSize, cursor } : { sessionID, limit: messagePageSize, order: "desc" },
|
||||
)
|
||||
})
|
||||
const first = await request(before)
|
||||
const pages = [first]
|
||||
@@ -632,14 +631,13 @@ export function createServerSession(
|
||||
}
|
||||
orphanParts.delete(sessionID)
|
||||
}
|
||||
setMeta("limit", sessionID, messages.length)
|
||||
setMeta("cursor", sessionID, merged.cursor)
|
||||
setMeta("complete", sessionID, merged.complete)
|
||||
setMeta("at", sessionID, Date.now())
|
||||
})
|
||||
}
|
||||
|
||||
const loadMessages = async (sessionID: string, limit: number, before?: string, mode?: "replace" | "prepend") => {
|
||||
const loadMessages = async (sessionID: string, before?: string, mode?: "replace" | "prepend") => {
|
||||
if (meta.loading[sessionID]) return
|
||||
const active = generation(sessionID)
|
||||
const load: MessageLoadState = {
|
||||
@@ -658,7 +656,7 @@ export function createServerSession(
|
||||
setMeta("loading", sessionID, true)
|
||||
let applied = false
|
||||
try {
|
||||
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
||||
const page = await fetchMessages(sessionID, before, () => resetMessageLoad(sessionID, load))
|
||||
const first = page.session.reduce<Message | undefined>(
|
||||
(oldest, message) => (!oldest || compareMessages(message, oldest) < 0 ? message : oldest),
|
||||
undefined,
|
||||
@@ -737,32 +735,30 @@ export function createServerSession(
|
||||
}
|
||||
}
|
||||
|
||||
const sync = (sessionID: string, options?: { force?: boolean; messageLimit?: number }) => {
|
||||
const sync = (sessionID: string, options?: { force?: boolean }) => {
|
||||
touch(sessionID)
|
||||
return runInflight(inflight, sessionID, async () => {
|
||||
const cached = data.message[sessionID] !== undefined && meta.limit[sessionID] !== undefined
|
||||
const cached = data.message[sessionID] !== undefined && meta.complete[sessionID] !== undefined
|
||||
const invalid = invalidated.has(sessionID)
|
||||
const revision = invalidationRevision
|
||||
if (cached && data.info[sessionID] && !invalid && !options?.force) return
|
||||
await Promise.all([
|
||||
resolve(sessionID, invalid ? { ...options, force: true } : options),
|
||||
cached && !invalid && !options?.force
|
||||
? Promise.resolve()
|
||||
: loadMessages(sessionID, options?.messageLimit ?? meta.limit[sessionID] ?? initialMessagePageSize),
|
||||
cached && !invalid && !options?.force ? Promise.resolve() : loadMessages(sessionID),
|
||||
])
|
||||
if (invalid && invalidationRevision === revision) invalidated.delete(sessionID)
|
||||
})
|
||||
}
|
||||
|
||||
const prefetch = async (sessionID: string, limit: number) => {
|
||||
const prefetch = async (sessionID: string, messageCount: number) => {
|
||||
touch(sessionID)
|
||||
await inflight.get(sessionID)
|
||||
if (
|
||||
Date.now() - (meta.at[sessionID] ?? 0) <= 15_000 &&
|
||||
(meta.complete[sessionID] || (data.message[sessionID]?.length ?? 0) >= limit)
|
||||
(meta.complete[sessionID] || (data.message[sessionID]?.length ?? 0) >= messageCount)
|
||||
)
|
||||
return
|
||||
await runInflight(inflight, sessionID, () => loadMessages(sessionID, limit))
|
||||
await runInflight(inflight, sessionID, () => loadMessages(sessionID))
|
||||
}
|
||||
|
||||
const eventSessionID = (event: { type: string; properties?: unknown }) => {
|
||||
@@ -1354,11 +1350,11 @@ export function createServerSession(
|
||||
setMeta("at", {})
|
||||
},
|
||||
prefetch,
|
||||
shouldPrefetch(sessionID: string, limit: number) {
|
||||
shouldPrefetch(sessionID: string, messageCount: number) {
|
||||
if (data.message[sessionID] === undefined) return true
|
||||
if (Date.now() - (meta.at[sessionID] ?? 0) > 15_000) return true
|
||||
if (meta.complete[sessionID]) return false
|
||||
return (meta.limit[sessionID] ?? 0) <= limit
|
||||
return (data.message[sessionID]?.length ?? 0) <= messageCount
|
||||
},
|
||||
fresh(sessionID: string, ttl: number) {
|
||||
return Date.now() - (meta.at[sessionID] ?? 0) <= ttl
|
||||
@@ -1440,14 +1436,14 @@ export function createServerSession(
|
||||
history: {
|
||||
more: (sessionID: string) =>
|
||||
data.message[sessionID] !== undefined &&
|
||||
meta.limit[sessionID] !== undefined &&
|
||||
meta.complete[sessionID] !== undefined &&
|
||||
!meta.complete[sessionID] &&
|
||||
!!meta.cursor[sessionID],
|
||||
loading: (sessionID: string) => meta.loading[sessionID] ?? false,
|
||||
async loadMore(sessionID: string, count = historyMessagePageSize) {
|
||||
async loadMore(sessionID: string) {
|
||||
touch(sessionID)
|
||||
if (meta.loading[sessionID] || meta.complete[sessionID] || !meta.cursor[sessionID]) return
|
||||
await loadMessages(sessionID, count, meta.cursor[sessionID], "prepend")
|
||||
await loadMessages(sessionID, meta.cursor[sessionID], "prepend")
|
||||
},
|
||||
},
|
||||
evict(sessionID: string) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ConfigAgentV1 } from "../../v1/config/agent.js"
|
||||
import { ConfigMigrateV1 } from "../../v1/config/migrate.js"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Permission } from "../../permission.js"
|
||||
import type { LocationPath } from "../../location-path.js"
|
||||
import type { LocationMutation } from "../../location-mutation.js"
|
||||
import type { ReadTool } from "../../tool/plugin/read.js"
|
||||
import type { EditTool } from "../../tool/plugin/edit.js"
|
||||
import { AbsolutePath } from "../../schema.js"
|
||||
@@ -28,7 +28,7 @@ const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info)
|
||||
const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info)
|
||||
const decodeConfig = Schema.decodeUnknownOption(Info)
|
||||
type PathAction =
|
||||
| LocationPath.ExternalDirectoryAuthorization["action"]
|
||||
| LocationMutation.ExternalDirectoryAuthorization["action"]
|
||||
| typeof ReadTool.name
|
||||
| typeof EditTool.name
|
||||
const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as LocationPath from "./location-path.js"
|
||||
export * as LocationMutation from "./location-mutation.js"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import path from "path"
|
||||
@@ -12,8 +12,8 @@ export const Kind = Schema.Literals(["file", "directory"])
|
||||
export type Kind = typeof Kind.Type
|
||||
|
||||
/**
|
||||
* Tool paths do not accept project references. Relative paths resolve from
|
||||
* the active Location. Paths outside its project require separate
|
||||
* Mutation paths do not accept project references. Relative paths resolve
|
||||
* from the active Location. Paths outside it require separate
|
||||
* `external_directory` approval.
|
||||
*/
|
||||
export const ResolveInput = Schema.Struct({
|
||||
@@ -49,13 +49,13 @@ export interface Target {
|
||||
export interface Interface {
|
||||
/**
|
||||
* Resolve a path and derive its permission resources. Relative paths resolve
|
||||
* from the Location. Paths outside its project require separate
|
||||
* `external_directory` approval. This does not approve access.
|
||||
* from the Location. Paths outside it require separate `external_directory`
|
||||
* approval. This does not approve the mutation.
|
||||
*/
|
||||
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, FSUtil.Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationPath") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
|
||||
|
||||
const slash = (value: string) => value.replaceAll("\\", "/")
|
||||
|
||||
@@ -65,13 +65,9 @@ const layer = Layer.effect(
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
|
||||
const resolve = Effect.fn("LocationPath.resolve")(function* (input: ResolveInput) {
|
||||
const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {
|
||||
const absolute = path.resolve(location.directory, input.path)
|
||||
const projectRoot = path.parse(location.project.directory).root
|
||||
if (
|
||||
FSUtil.contains(location.directory, absolute) ||
|
||||
(location.project.directory !== projectRoot && FSUtil.contains(location.project.directory, absolute))
|
||||
) {
|
||||
if (FSUtil.contains(location.directory, absolute)) {
|
||||
return {
|
||||
absolute,
|
||||
resource: slash(path.relative(location.directory, absolute) || "."),
|
||||
@@ -19,7 +19,7 @@ import { Image } from "./image.js"
|
||||
import { LocationWatcher } from "./filesystem/location-watcher.js"
|
||||
import { Integration } from "./integration.js"
|
||||
import { Location } from "./location.js"
|
||||
import { LocationPath } from "./location-path.js"
|
||||
import { LocationMutation } from "./location-mutation.js"
|
||||
import { LocationServiceMap } from "./location-service-map.js"
|
||||
import { ModelResolver } from "./model-resolver.js"
|
||||
import { MCP } from "./mcp/index.js"
|
||||
@@ -76,7 +76,7 @@ const locationServiceNodes = [
|
||||
Skill.node,
|
||||
InstructionBuiltIns.node,
|
||||
InstructionDiscovery.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
FileMutation.node,
|
||||
Formatter.node,
|
||||
MCP.node,
|
||||
|
||||
@@ -32,7 +32,7 @@ import { InstructionDiscovery } from "../instruction-discovery.js"
|
||||
import { Integration } from "../integration.js"
|
||||
import { KV } from "../kv.js"
|
||||
import { Location } from "../location.js"
|
||||
import { LocationPath } from "../location-path.js"
|
||||
import { LocationMutation } from "../location-mutation.js"
|
||||
import { ModelsDev } from "../models-dev.js"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import { Permission } from "../permission.js"
|
||||
@@ -92,7 +92,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
||||
const integration = yield* Integration.Service
|
||||
const kv = yield* KV.Service
|
||||
const location = yield* Location.Service
|
||||
const locationMutation = yield* LocationPath.Service
|
||||
const locationMutation = yield* LocationMutation.Service
|
||||
const models = yield* ModelsDev.Service
|
||||
const npm = yield* Npm.Service
|
||||
const permission = yield* Permission.Service
|
||||
@@ -129,7 +129,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
|
||||
Context.make(Integration.Service, integration),
|
||||
Context.make(KV.Service, kv),
|
||||
Context.make(Location.Service, location),
|
||||
Context.make(LocationPath.Service, locationMutation),
|
||||
Context.make(LocationMutation.Service, locationMutation),
|
||||
Context.make(ModelsDev.Service, models),
|
||||
Context.make(Npm.Service, npm),
|
||||
Context.make(Permission.Service, permission),
|
||||
@@ -173,7 +173,7 @@ export const requirements = LayerNode.group([
|
||||
Integration.node,
|
||||
KV.node,
|
||||
Location.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
ModelsDev.node,
|
||||
Npm.node,
|
||||
Permission.node,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Environment } from "../../environment/index.js"
|
||||
import { FileMutation } from "../../file-mutation.js"
|
||||
import { Formatter } from "../../formatter.js"
|
||||
import { Location } from "../../location.js"
|
||||
import { LocationPath } from "../../location-path.js"
|
||||
import { LocationMutation } from "../../location-mutation.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
import { fileDiff } from "./file-diff.js"
|
||||
|
||||
@@ -110,7 +110,7 @@ const findLineOccurrences = (content: string, search: string) => {
|
||||
export const Plugin = {
|
||||
id: "opencode.tool.edit",
|
||||
effect: Effect.fn("EditTool.Plugin")(function* (ctx: PluginContext) {
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const fileMutation = yield* FileMutation.Service
|
||||
const environment = yield* Environment.Service
|
||||
const formatter = yield* Formatter.Service
|
||||
@@ -148,7 +148,7 @@ export const Plugin = {
|
||||
const external = target.externalDirectory
|
||||
if (external) {
|
||||
yield* permission.assert({
|
||||
...LocationPath.externalDirectoryPermission(external),
|
||||
...LocationMutation.externalDirectoryPermission(external),
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: permissionSource,
|
||||
|
||||
@@ -7,7 +7,7 @@ import path from "path"
|
||||
import { Environment } from "../../environment/index.js"
|
||||
import { FileSystem } from "../../filesystem.js"
|
||||
import { Location } from "../../location.js"
|
||||
import { LocationPath } from "../../location-path.js"
|
||||
import { LocationMutation } from "../../location-mutation.js"
|
||||
import { Ripgrep } from "../../ripgrep.js"
|
||||
import { RelativePath } from "../../schema.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
@@ -45,7 +45,7 @@ export const Plugin = {
|
||||
const environment = yield* Environment.Service
|
||||
const ripgrep = yield* Ripgrep.Service
|
||||
const location = yield* Location.Service
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const permission = yield* Permission.Service
|
||||
|
||||
yield* ctx.tool
|
||||
@@ -64,7 +64,7 @@ export const Plugin = {
|
||||
const external = target.externalDirectory
|
||||
if (external)
|
||||
yield* permission.assert({
|
||||
...LocationPath.externalDirectoryPermission(external),
|
||||
...LocationMutation.externalDirectoryPermission(external),
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
|
||||
@@ -7,7 +7,7 @@ import path from "path"
|
||||
import { Environment } from "../../environment/index.js"
|
||||
import { FileSystem } from "../../filesystem.js"
|
||||
import { Location } from "../../location.js"
|
||||
import { LocationPath } from "../../location-path.js"
|
||||
import { LocationMutation } from "../../location-mutation.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
import { Ripgrep } from "../../ripgrep.js"
|
||||
import { RelativePath } from "../../schema.js"
|
||||
@@ -61,7 +61,7 @@ export const Plugin = {
|
||||
const environment = yield* Environment.Service
|
||||
const ripgrep = yield* Ripgrep.Service
|
||||
const location = yield* Location.Service
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const permission = yield* Permission.Service
|
||||
|
||||
yield* ctx.tool
|
||||
@@ -79,7 +79,7 @@ export const Plugin = {
|
||||
const target = yield* mutation.resolve({ path: input.path ?? "." })
|
||||
if (target.externalDirectory)
|
||||
yield* permission.assert({
|
||||
...LocationPath.externalDirectoryPermission(target.externalDirectory),
|
||||
...LocationMutation.externalDirectoryPermission(target.externalDirectory),
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Environment } from "../../environment/index.js"
|
||||
import { Formatter } from "../../formatter.js"
|
||||
import { FileMutation } from "../../file-mutation.js"
|
||||
import { Location } from "../../location.js"
|
||||
import { LocationPath } from "../../location-path.js"
|
||||
import { LocationMutation } from "../../location-mutation.js"
|
||||
import { Patch } from "@opencode-ai/util/patch"
|
||||
import { Permission } from "../../permission.js"
|
||||
import DESCRIPTION from "../patch.txt"
|
||||
@@ -46,29 +46,29 @@ export const toModelOutput = (output: Output) =>
|
||||
|
||||
type Prepared =
|
||||
| (Extract<Patch.Hunk, { readonly type: "add" }> & {
|
||||
readonly target: LocationPath.Target
|
||||
readonly target: LocationMutation.Target
|
||||
readonly content: string
|
||||
readonly before: string
|
||||
readonly after: string
|
||||
})
|
||||
| (Extract<Patch.Hunk, { readonly type: "delete" }> & {
|
||||
readonly target: LocationPath.Target
|
||||
readonly target: LocationMutation.Target
|
||||
readonly before: string
|
||||
readonly after: string
|
||||
})
|
||||
| (Extract<Patch.Hunk, { readonly type: "update" }> & {
|
||||
readonly target: LocationPath.Target
|
||||
readonly target: LocationMutation.Target
|
||||
readonly content: string
|
||||
readonly before: string
|
||||
readonly after: string
|
||||
readonly moveTarget?: LocationPath.Target
|
||||
readonly moveTarget?: LocationMutation.Target
|
||||
})
|
||||
|
||||
export const Plugin = {
|
||||
id: "opencode.tool.patch",
|
||||
effect: Effect.fn("PatchTool.Plugin")(function* (ctx: PluginContext) {
|
||||
const environment = yield* Environment.Service
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const fileMutation = yield* FileMutation.Service
|
||||
const formatter = yield* Formatter.Service
|
||||
const location = yield* Location.Service
|
||||
@@ -116,7 +116,7 @@ export const Plugin = {
|
||||
const target = yield* mutation.resolve({ path: value, kind: "file" })
|
||||
if (!target.externalDirectory) return target
|
||||
yield* permission.assert({
|
||||
...LocationPath.externalDirectoryPermission(target.externalDirectory),
|
||||
...LocationMutation.externalDirectoryPermission(target.externalDirectory),
|
||||
metadata: {
|
||||
filepath: target.absolute,
|
||||
parentDir: target.externalDirectory.directory,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Location } from "../../location.js"
|
||||
import { LocationPath } from "../../location-path.js"
|
||||
import { LocationMutation } from "../../location-mutation.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
import { SessionInstructions } from "../../session/instructions.js"
|
||||
import { AbsolutePath } from "../../schema.js"
|
||||
@@ -32,7 +32,7 @@ export const Plugin = {
|
||||
id: "opencode.tool.read",
|
||||
effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) {
|
||||
const reader = yield* ReadToolFileSystem.Service
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const permission = yield* Permission.Service
|
||||
const sessionInstructions = yield* SessionInstructions.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
@@ -58,7 +58,7 @@ export const Plugin = {
|
||||
const external = target.externalDirectory
|
||||
if (external)
|
||||
yield* permission.assert({
|
||||
...LocationPath.externalDirectoryPermission(external),
|
||||
...LocationMutation.externalDirectoryPermission(external),
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin
|
||||
import { Deferred, Effect, Schema, Scope } from "effect"
|
||||
import { Config } from "../../config.js"
|
||||
import { Environment } from "../../environment/index.js"
|
||||
import { LocationPath } from "../../location-path.js"
|
||||
import { LocationMutation } from "../../location-mutation.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
import { PluginRuntime } from "../../plugin/runtime.js"
|
||||
import { NonNegativeInt } from "../../schema.js"
|
||||
@@ -84,7 +84,7 @@ export const Plugin = {
|
||||
const runtime = yield* PluginRuntime.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const environment = yield* Environment.Service
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const shell = yield* Shell.Service
|
||||
const permission = yield* Permission.Service
|
||||
const config = yield* Config.Service
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Bom } from "@opencode-ai/util/bom"
|
||||
import { Environment } from "../../environment/index.js"
|
||||
import { FileMutation } from "../../file-mutation.js"
|
||||
import { Formatter } from "../../formatter.js"
|
||||
import { LocationPath } from "../../location-path.js"
|
||||
import { LocationMutation } from "../../location-mutation.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
import { fileDiff } from "./file-diff.js"
|
||||
|
||||
@@ -46,7 +46,7 @@ export const toModelOutput = (output: Output) =>
|
||||
export const Plugin = {
|
||||
id: "opencode.tool.write",
|
||||
effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) {
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const fileMutation = yield* FileMutation.Service
|
||||
const environment = yield* Environment.Service
|
||||
const formatter = yield* Formatter.Service
|
||||
@@ -72,7 +72,7 @@ export const Plugin = {
|
||||
const external = target.externalDirectory
|
||||
if (external)
|
||||
yield* permission.assert({
|
||||
...LocationPath.externalDirectoryPermission(external),
|
||||
...LocationMutation.externalDirectoryPermission(external),
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { FileMutation } from "@opencode-ai/core/file-mutation"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { type EnvironmentFilesTransform, transformEnvironmentFiles } from "./fixture/environment"
|
||||
import { location } from "./fixture/location"
|
||||
@@ -20,7 +20,7 @@ function provide(directory: string, transformFiles: EnvironmentFilesTransform =
|
||||
Location.Service.of(location({ directory: AbsolutePath.make(directory) })),
|
||||
)
|
||||
return Effect.provide(
|
||||
AppNodeBuilder.build(LayerNode.group([LocationPath.node, FileMutation.node]), [
|
||||
AppNodeBuilder.build(LayerNode.group([LocationMutation.node, FileMutation.node]), [
|
||||
[Location.node, activeLocation],
|
||||
[Environment.node, transformEnvironmentFiles(activeLocation, transformFiles)],
|
||||
]),
|
||||
@@ -40,7 +40,7 @@ describe("FileMutation", () => {
|
||||
Effect.gen(function* () {
|
||||
const targetPath = path.join(directory, "hello.txt")
|
||||
yield* Effect.promise(() => fs.writeFile(targetPath, "before"))
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: "hello.txt" })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" })
|
||||
|
||||
expect(yield* (yield* FileMutation.Service).write({ target, content: "after" })).toEqual({
|
||||
operation: "write",
|
||||
@@ -56,7 +56,7 @@ describe("FileMutation", () => {
|
||||
it.live("writes a prospective internal file and creates parent directories", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const target = yield* (yield* LocationPath.Service).resolve({
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({
|
||||
path: path.join("src", "nested", "hello.txt"),
|
||||
})
|
||||
const result = yield* (yield* FileMutation.Service).write({ target, content: "hello" })
|
||||
@@ -77,8 +77,8 @@ describe("FileMutation", () => {
|
||||
Effect.gen(function* () {
|
||||
const preservedPath = path.join(directory, "preserved.txt")
|
||||
yield* Effect.promise(() => fs.writeFile(preservedPath, "\uFEFFbefore"))
|
||||
const preserved = yield* (yield* LocationPath.Service).resolve({ path: "preserved.txt" })
|
||||
const created = yield* (yield* LocationPath.Service).resolve({ path: "created.txt" })
|
||||
const preserved = yield* (yield* LocationMutation.Service).resolve({ path: "preserved.txt" })
|
||||
const created = yield* (yield* LocationMutation.Service).resolve({ path: "created.txt" })
|
||||
const files = yield* FileMutation.Service
|
||||
|
||||
yield* files.writeTextPreservingBom({ target: preserved, content: "\uFEFFafter" })
|
||||
@@ -95,7 +95,7 @@ describe("FileMutation", () => {
|
||||
withTmp((outside) =>
|
||||
Effect.gen(function* () {
|
||||
const targetPath = path.join(outside, "external.txt")
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
||||
const result = yield* (yield* FileMutation.Service).write({ target, content: "external" })
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -133,7 +133,7 @@ describe("FileMutation", () => {
|
||||
)
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const files = yield* FileMutation.Service
|
||||
const firstPlan = yield* mutation.resolve({ path: "shared.txt" })
|
||||
const secondPlan = yield* mutation.resolve({ path: "shared.txt" })
|
||||
@@ -222,7 +222,7 @@ describe("FileMutation", () => {
|
||||
)
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const mutation = yield* LocationPath.Service
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const files = yield* FileMutation.Service
|
||||
const firstPlan = yield* mutation.resolve({ path: "first.txt" })
|
||||
const secondPlan = yield* mutation.resolve({ path: "second.txt" })
|
||||
|
||||
+18
-59
@@ -4,26 +4,18 @@ import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
import { location } from "./fixture/location"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
function provide(directory: string, projectDirectory = directory) {
|
||||
function provide(directory: string) {
|
||||
return Effect.provide(
|
||||
LayerNode.compile(LocationPath.node, [
|
||||
LayerNode.compile(LocationMutation.node, [
|
||||
[
|
||||
Location.node,
|
||||
Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(
|
||||
location(
|
||||
{ directory: AbsolutePath.make(directory) },
|
||||
{ projectDirectory: AbsolutePath.make(projectDirectory) },
|
||||
),
|
||||
),
|
||||
),
|
||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
|
||||
],
|
||||
]),
|
||||
)
|
||||
@@ -36,13 +28,13 @@ function withTmp<A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) {
|
||||
).pipe(Effect.flatMap((tmp) => f(tmp.path)))
|
||||
}
|
||||
|
||||
describe("LocationPath", () => {
|
||||
describe("LocationMutation", () => {
|
||||
it.live("resolves an active relative existing file target", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const targetPath = path.join(directory, "hello.txt")
|
||||
yield* Effect.promise(() => fs.writeFile(targetPath, "hello"))
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: "hello.txt" })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: "hello.txt" })
|
||||
|
||||
expect(target).toMatchObject({
|
||||
absolute: targetPath,
|
||||
@@ -57,7 +49,7 @@ describe("LocationPath", () => {
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(directory, "src")))
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: path.join("src", "new.txt") })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: path.join("src", "new.txt") })
|
||||
expect(target).toMatchObject({
|
||||
absolute: path.join(directory, "src", "new.txt"),
|
||||
resource: "src/new.txt",
|
||||
@@ -66,43 +58,10 @@ describe("LocationPath", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("does not require external authorization inside the project but outside the active directory", () =>
|
||||
withTmp((project) =>
|
||||
Effect.gen(function* () {
|
||||
const directory = path.join(project, "packages", "app")
|
||||
const targetPath = path.join(project, "README.md")
|
||||
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
|
||||
yield* Effect.promise(() => fs.writeFile(targetPath, "hello"))
|
||||
|
||||
const locationPath = yield* LocationPath.Service
|
||||
const target = yield* locationPath.resolve({ path: targetPath })
|
||||
|
||||
expect(target).toMatchObject({
|
||||
absolute: targetPath,
|
||||
resource: "../../README.md",
|
||||
})
|
||||
expect(target.externalDirectory).toBeUndefined()
|
||||
}).pipe(provide(path.join(project, "packages", "app"), project)),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("does not treat a filesystem-root project fallback as internal", () =>
|
||||
withTmp((directory) =>
|
||||
withTmp((outside) =>
|
||||
Effect.gen(function* () {
|
||||
const locationPath = yield* LocationPath.Service
|
||||
const target = yield* locationPath.resolve({ path: path.join(outside, "target.txt") })
|
||||
|
||||
expect(target.externalDirectory).toBeDefined()
|
||||
}).pipe(provide(directory, path.parse(directory).root)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("requires external-directory authorization for a relative lexical escape", () =>
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: "../outside.txt" })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: "../outside.txt" })
|
||||
const root = path.dirname(directory)
|
||||
expect(target).toMatchObject({
|
||||
absolute: path.join(root, "outside.txt"),
|
||||
@@ -125,7 +84,7 @@ describe("LocationPath", () => {
|
||||
await fs.mkdir(outside)
|
||||
await fs.symlink(outside, path.join(directory, "escape"))
|
||||
})
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: path.join("escape", "new.txt") })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: path.join("escape", "new.txt") })
|
||||
expect(target).toMatchObject({
|
||||
absolute: path.join(directory, "escape", "new.txt"),
|
||||
resource: "escape/new.txt",
|
||||
@@ -145,7 +104,7 @@ describe("LocationPath", () => {
|
||||
await fs.symlink(path.join(directory, "actual"), path.join(directory, "linked"))
|
||||
})
|
||||
|
||||
expect(yield* (yield* LocationPath.Service).resolve({ path: "linked/new.txt" })).toMatchObject({
|
||||
expect(yield* (yield* LocationMutation.Service).resolve({ path: "linked/new.txt" })).toMatchObject({
|
||||
absolute: path.join(directory, "linked", "new.txt"),
|
||||
resource: "linked/new.txt",
|
||||
})
|
||||
@@ -157,7 +116,7 @@ describe("LocationPath", () => {
|
||||
withTmp((directory) =>
|
||||
Effect.gen(function* () {
|
||||
const targetPath = path.join(directory, "new.txt")
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
||||
expect(target).toMatchObject({
|
||||
absolute: targetPath,
|
||||
resource: "new.txt",
|
||||
@@ -172,7 +131,7 @@ describe("LocationPath", () => {
|
||||
withTmp((outside) =>
|
||||
Effect.gen(function* () {
|
||||
const targetPath = path.join(outside, "new.txt")
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
||||
const root = outside
|
||||
expect(target).toMatchObject({
|
||||
absolute: path.join(root, "new.txt"),
|
||||
@@ -193,7 +152,7 @@ describe("LocationPath", () => {
|
||||
Effect.gen(function* () {
|
||||
const targetPath = path.join(outside, "existing.txt")
|
||||
yield* Effect.promise(() => fs.writeFile(targetPath, "existing"))
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
||||
expect(target).toMatchObject({ absolute: targetPath })
|
||||
expect(target.externalDirectory?.directory).toBe(outside)
|
||||
}).pipe(provide(directory)),
|
||||
@@ -205,7 +164,7 @@ describe("LocationPath", () => {
|
||||
withTmp((directory) =>
|
||||
withTmp((outside) =>
|
||||
Effect.gen(function* () {
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: outside, kind: "file" })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: outside, kind: "file" })
|
||||
expect(target.externalDirectory).toMatchObject({
|
||||
directory: path.dirname(outside),
|
||||
resource: path.join(path.dirname(outside), "*").replaceAll("\\", "/"),
|
||||
@@ -220,7 +179,7 @@ describe("LocationPath", () => {
|
||||
withTmp((outside) =>
|
||||
Effect.gen(function* () {
|
||||
const targetPath = path.join(outside, "new", "nested", "file.txt")
|
||||
const target = yield* (yield* LocationPath.Service).resolve({ path: targetPath })
|
||||
const target = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
|
||||
const parent = path.dirname(targetPath)
|
||||
expect(target.externalDirectory).toMatchObject({
|
||||
directory: parent,
|
||||
@@ -231,9 +190,9 @@ describe("LocationPath", () => {
|
||||
),
|
||||
)
|
||||
|
||||
test("ignores unknown path input fields", () => {
|
||||
expect(Object.keys(LocationPath.ResolveInput.fields)).toEqual(["path", "kind"])
|
||||
expect(Schema.decodeUnknownSync(LocationPath.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({
|
||||
test("ignores unknown mutation input fields", () => {
|
||||
expect(Object.keys(LocationMutation.ResolveInput.fields)).toEqual(["path", "kind"])
|
||||
expect(Schema.decodeUnknownSync(LocationMutation.ResolveInput)({ path: "README.md", reference: "docs" })).toEqual({
|
||||
path: "README.md",
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,7 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Image } from "@opencode-ai/core/image"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
@@ -43,7 +43,7 @@ const readToolNode = makeLocationNode({
|
||||
deps: [
|
||||
Tool.node,
|
||||
ReadToolFileSystem.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
Image.node,
|
||||
Permission.node,
|
||||
SessionInstructions.node,
|
||||
@@ -65,7 +65,7 @@ const testLayer = AppNodeBuilder.build(
|
||||
Session.node,
|
||||
Location.node,
|
||||
FSUtil.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
ReadToolFileSystem.node,
|
||||
readToolNode,
|
||||
Tool.node,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { FileMutation } from "@opencode-ai/core/file-mutation"
|
||||
import { Formatter } from "@opencode-ai/core/formatter"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
@@ -27,7 +27,7 @@ const editToolNode = makeLocationNode({
|
||||
layer: Layer.effectDiscard(registerToolPlugin(EditTool.Plugin)),
|
||||
deps: [
|
||||
Tool.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
FileMutation.node,
|
||||
Environment.node,
|
||||
Formatter.node,
|
||||
@@ -84,7 +84,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: Tool.Interface) =
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([Tool.node, Tool.node, LocationPath.node, FileMutation.node, editToolNode]),
|
||||
LayerNode.group([Tool.node, Tool.node, LocationMutation.node, FileMutation.node, editToolNode]),
|
||||
[
|
||||
[
|
||||
Environment.node,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Formatter } from "@opencode-ai/core/formatter"
|
||||
import { FileMutation } from "@opencode-ai/core/file-mutation"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
@@ -27,7 +27,7 @@ const patchToolNode = makeLocationNode({
|
||||
layer: Layer.effectDiscard(registerToolPlugin(PatchTool.Plugin)),
|
||||
deps: [
|
||||
Tool.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
FileMutation.node,
|
||||
Environment.node,
|
||||
Formatter.node,
|
||||
@@ -99,7 +99,7 @@ const withTool = <A, E, R>(
|
||||
return yield* body(yield* Tool.Service)
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
AppNodeBuilder.build(LayerNode.group([Tool.node, LocationPath.node, FileMutation.node, patchToolNode]), [
|
||||
AppNodeBuilder.build(LayerNode.group([Tool.node, LocationMutation.node, FileMutation.node, patchToolNode]), [
|
||||
[
|
||||
Environment.node,
|
||||
transformEnvironmentFiles(activeLocation, (files) => ({
|
||||
@@ -920,7 +920,7 @@ describe("PatchTool", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("treats a sibling path inside the project worktree as internal", () =>
|
||||
it.live("treats a sibling path inside the project worktree as external to the Location", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
@@ -939,8 +939,9 @@ describe("PatchTool", () => {
|
||||
call("*** Begin Patch\n*** Update File: ../sibling.txt\n@@\n-before\n+after\n*** End Patch"),
|
||||
),
|
||||
).toMatchObject({ status: "completed" })
|
||||
expect(assertions.map((input) => input.action)).toEqual(["edit"])
|
||||
expect(assertions[0]?.resources).toEqual(["../sibling.txt"])
|
||||
expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
|
||||
expect(assertions[0]?.resources).toEqual([path.join(tmp.path, "*").replaceAll("\\", "/")])
|
||||
expect(assertions[1]?.resources).toEqual([target.replaceAll("\\", "/")])
|
||||
expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\n")
|
||||
}),
|
||||
tmp.path,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Permission } from "@opencode-ai/core/permission"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { location } from "./fixture/location"
|
||||
import { Tool } from "@opencode-ai/core/tool"
|
||||
import { ReadTool } from "@opencode-ai/core/tool/plugin/read"
|
||||
@@ -32,7 +32,7 @@ const readToolNode = makeLocationNode({
|
||||
deps: [
|
||||
Tool.node,
|
||||
ReadToolFileSystem.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
Image.node,
|
||||
Permission.node,
|
||||
SessionInstructions.node,
|
||||
@@ -107,8 +107,8 @@ const locationLayer = Layer.succeed(
|
||||
Location.Service.of(location({ directory: AbsolutePath.make(process.cwd()) })),
|
||||
)
|
||||
const mutation = Layer.succeed(
|
||||
LocationPath.Service,
|
||||
LocationPath.Service.of({
|
||||
LocationMutation.Service,
|
||||
LocationMutation.Service.of({
|
||||
resolve: (input) => {
|
||||
const absolute = path.resolve(process.cwd(), input.path)
|
||||
const external = path.isAbsolute(input.path) && !FSUtil.contains(process.cwd(), absolute)
|
||||
@@ -141,7 +141,7 @@ const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
|
||||
[Permission.node, permission],
|
||||
[Config.node, config],
|
||||
[Image.node, imageLayer],
|
||||
[LocationPath.node, mutation],
|
||||
[LocationMutation.node, mutation],
|
||||
[FSUtil.node, testFileSystem],
|
||||
[Location.node, locationLayer],
|
||||
[Global.node, Global.layerWith({ data: Global.Path.data })],
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
@@ -25,12 +25,12 @@ import { executeTool, registerToolPlugin, toolIdentity } from "./lib/tool"
|
||||
const globToolNode = makeLocationNode({
|
||||
name: "test/glob-tool-plugin",
|
||||
layer: Layer.effectDiscard(registerToolPlugin(GlobTool.Plugin)),
|
||||
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationPath.node, Permission.node],
|
||||
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationMutation.node, Permission.node],
|
||||
})
|
||||
const grepToolNode = makeLocationNode({
|
||||
name: "test/grep-tool-plugin",
|
||||
layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)),
|
||||
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationPath.node, Permission.node],
|
||||
deps: [Tool.node, Environment.node, Ripgrep.node, Location.node, LocationMutation.node, Permission.node],
|
||||
})
|
||||
const sessionID = Session.ID.make("ses_search_tool_test")
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
@@ -132,7 +132,7 @@ const shellPluginSupervisor = makeLocationNode({
|
||||
deps: [
|
||||
Config.node,
|
||||
Environment.node,
|
||||
LocationPath.node,
|
||||
LocationMutation.node,
|
||||
Permission.node,
|
||||
PluginRuntime.node,
|
||||
Shell.node,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationPath } from "@opencode-ai/core/location-path"
|
||||
import { LocationMutation } from "@opencode-ai/core/location-mutation"
|
||||
import { Permission } from "@opencode-ai/core/permission"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
@@ -25,7 +25,7 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "
|
||||
const writeToolNode = makeLocationNode({
|
||||
name: "test/write-tool-plugin",
|
||||
layer: Layer.effectDiscard(registerToolPlugin(WriteTool.Plugin)),
|
||||
deps: [Tool.node, LocationPath.node, FileMutation.node, Environment.node, Formatter.node, Permission.node],
|
||||
deps: [Tool.node, LocationMutation.node, FileMutation.node, Environment.node, Formatter.node, Permission.node],
|
||||
})
|
||||
|
||||
const sessionID = Session.ID.make("ses_write_tool_test")
|
||||
@@ -72,7 +72,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: Tool.Interface) =
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
AppNodeBuilder.build(
|
||||
LayerNode.group([Tool.node, Tool.node, LocationPath.node, FileMutation.node, writeToolNode]),
|
||||
LayerNode.group([Tool.node, Tool.node, LocationMutation.node, FileMutation.node, writeToolNode]),
|
||||
[
|
||||
[
|
||||
Environment.node,
|
||||
|
||||
Reference in New Issue
Block a user