refactor: rename State drafts to editors (#46876)

This commit is contained in:
Kit Langton
2026-09-02 14:39:02 -04:00
committed by GitHub
parent 318a82f784
commit 4772b6a3e8
139 changed files with 1064 additions and 1063 deletions
+10 -10
View File
@@ -34,7 +34,7 @@ type Data = {
default?: ID
}
export type Draft = {
export type Editor = {
list: () => readonly Info[]
get: (id: ID) => Info | undefined
default: (id: ID | undefined) => void
@@ -42,7 +42,7 @@ export type Draft = {
remove: (id: ID) => void
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly get: (id: ID) => Effect.Effect<Info | undefined>
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
readonly select: (id?: ID | string) => Effect.Effect<Selection>
@@ -62,29 +62,29 @@ const layer = Layer.effect(
{ action: "external_directory", resource: path.join(global.tmp, "*"), effect: "allow" },
{ action: "external_directory", resource: path.join(global.config, "*"), effect: "allow" },
]
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "agent",
initial: () => ({ agents: new Map() }),
draft: (draft) => ({
list: () => Array.fromIterable(draft.agents.values()) as Info[],
get: (id) => draft.agents.get(id),
editor: (editor) => ({
list: () => Array.fromIterable(editor.agents.values()) as Info[],
get: (id) => editor.agents.get(id),
default: (id) => {
draft.default = id
editor.default = id
},
update: (id, fn) => {
const defaults = Info.default(id)
const current =
draft.agents.get(id) ??
editor.agents.get(id) ??
({
...defaults,
permissions: [...defaults.permissions, ...permissions],
} as Types.DeepMutable<Info>)
if (!draft.agents.has(id)) draft.agents.set(id, current)
if (!editor.agents.has(id)) editor.agents.set(id, current)
fn(current)
current.id = id
},
remove: (id) => {
draft.agents.delete(id)
editor.agents.delete(id)
},
}),
notify: () => bus.publish(Agent.Event.Updated, {}).pipe(Effect.asVoid),
+16 -16
View File
@@ -23,7 +23,7 @@ type Data = {
defaultModel?: DefaultModel
}
export type Draft = {
export type Editor = {
provider: {
list: () => readonly ProviderRecord[]
get: (providerID: Provider.ID) => ProviderRecord | undefined
@@ -41,7 +41,7 @@ export type Draft = {
}
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly provider: {
readonly get: (providerID: Provider.ID) => Effect.Effect<Provider.Info | undefined>
readonly all: () => Effect.Effect<Provider.Info[]>
@@ -82,39 +82,39 @@ const layer = Layer.effect(
} satisfies Model.Info
}
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "catalog",
initial: () => ({ providers: new Map() }),
draft: (draft) => {
const result: Draft = {
editor: (editor) => {
const result: Editor = {
provider: {
list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
get: (providerID) => draft.providers.get(providerID),
list: () => Array.fromIterable(editor.providers.values()) as ProviderRecord[],
get: (providerID) => editor.providers.get(providerID),
update: (providerID, fn) => {
let current = draft.providers.get(providerID)
let current = editor.providers.get(providerID)
if (!current) {
current = {
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
}
draft.providers.set(providerID, current)
editor.providers.set(providerID, current)
}
fn(current.provider)
},
remove: (providerID) => {
draft.providers.delete(providerID)
editor.providers.delete(providerID)
},
},
model: {
get: (providerID, modelID) => draft.providers.get(providerID)?.models.get(modelID),
get: (providerID, modelID) => editor.providers.get(providerID)?.models.get(modelID),
update: (providerID, modelID, fn) => {
let record = draft.providers.get(providerID)
let record = editor.providers.get(providerID)
if (!record) {
record = {
provider: Provider.Info.empty(providerID) as Provider.MutableInfo,
models: new Map<Model.ID, Model.MutableInfo>(),
}
draft.providers.set(providerID, record)
editor.providers.set(providerID, record)
}
const model = record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
if (!record.models.has(modelID)) record.models.set(modelID, model)
@@ -123,12 +123,12 @@ const layer = Layer.effect(
model.providerID = providerID
},
remove: (providerID, modelID) => {
draft.providers.get(providerID)?.models.delete(modelID)
editor.providers.get(providerID)?.models.delete(modelID)
},
default: {
get: () => draft.defaultModel,
get: () => editor.defaultModel,
set: (providerID, modelID) => {
draft.defaultModel = { providerID, modelID }
editor.defaultModel = { providerID, modelID }
},
},
},
+5 -5
View File
@@ -25,7 +25,7 @@ export interface Definition {
readonly execute: (input: Invocation) => Effect.Effect<void, unknown>
}
export type Draft = {
export type Editor = {
add: (definition: Definition) => void
}
@@ -39,7 +39,7 @@ export class ExecutionError extends Schema.TaggedError<ExecutionError>()("Comman
message: Schema.String,
}) {}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly get: (name: string) => Effect.Effect<Info | undefined>
readonly list: () => Effect.Effect<Info[]>
readonly execute: (input: {
@@ -54,11 +54,11 @@ export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const bus = yield* Bus.Service
const state = State.create<Map<string, Definition>, Draft>({
const state = State.create<Map<string, Definition>, Editor>({
name: "command",
initial: () => new Map(),
draft: (draft) => ({
add: (definition) => draft.set(definition.name, definition),
editor: (editor) => ({
add: (definition) => editor.set(definition.name, definition),
}),
notify: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
})
+7 -7
View File
@@ -83,27 +83,27 @@ export const Plugin = define({
Effect.forkScoped({ startImmediately: true }),
)
loaded.documents = yield* load()
yield* ctx.agent.transform((draft) => {
yield* ctx.agent.transform((editor) => {
const permissions = expandPermissions(
loaded.documents.flatMap((document) => document.info.permissions ?? []),
global.home,
)
const configuredDefault = Config.latest(loaded.documents, "default_agent")
if (configuredDefault !== undefined) draft.default(Agent.ID.make(configuredDefault))
for (const current of draft.list()) {
draft.update(current.id, (agent) => agent.permissions.push(...permissions))
if (configuredDefault !== undefined) editor.default(Agent.ID.make(configuredDefault))
for (const current of editor.list()) {
editor.update(current.id, (agent) => agent.permissions.push(...permissions))
}
for (const document of loaded.documents) {
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
const agentID = Agent.ID.make(id)
if (item.disabled) {
draft.remove(agentID)
editor.remove(agentID)
continue
}
const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => {
const exists = editor.get(agentID) !== undefined
editor.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...permissions)
if (item.model !== undefined)
agent.model = {
+2 -2
View File
@@ -64,10 +64,10 @@ export const Plugin = define({
Effect.forkScoped({ startImmediately: true }),
)
loaded.documents = yield* load()
yield* ctx.command.transform((draft) => {
yield* ctx.command.transform((editor) => {
for (const document of loaded.documents) {
for (const [name, command] of Object.entries(document.commands ?? {})) {
draft.add({
editor.add({
name,
description: command.description,
execute: (input) =>
@@ -12,10 +12,10 @@ export const Plugin = define({
const config = yield* Config.Service
const compaction = yield* SessionCompaction.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, compaction.reload())
yield* compaction.transform((draft) => {
yield* compaction.transform((editor) => {
for (const entry of loaded.entries) {
if (entry.type !== "document" || !entry.info.compaction) continue
draft.configure({
editor.configure({
...(entry.info.compaction.auto === undefined ? {} : { auto: entry.info.compaction.auto }),
...(entry.info.compaction.buffer === undefined ? {} : { buffer: entry.info.compaction.buffer }),
...(entry.info.compaction.keep?.tokens === undefined ? {} : { tokens: entry.info.compaction.keep.tokens }),
+4 -4
View File
@@ -24,7 +24,7 @@ export const Plugin = define({
const processes = yield* AppProcess.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, formatter.reload())
yield* formatter.transform((draft) => {
yield* formatter.transform((editor) => {
const configured = Config.latest(loaded.entries, "formatter")
if (!configured) return
const builtIns = make({
@@ -35,12 +35,12 @@ export const Plugin = define({
processes,
bin: global.bin,
})
builtIns.forEach(draft.set)
builtIns.forEach(editor.set)
if (configured === true) return
for (const [name, entry] of Object.entries(configured)) {
if (entry.disabled) {
draft.remove(name)
editor.remove(name)
continue
}
const builtIn = builtIns.find((formatter) => formatter.name === name)
@@ -51,7 +51,7 @@ export const Plugin = define({
enabled:
builtIn && !entry.command ? builtIn.enabled : Effect.succeed(entry.command ? [...entry.command] : false),
}
draft.set(current)
editor.set(current)
}
})
}),
+2 -2
View File
@@ -12,12 +12,12 @@ export const Plugin = define({
const config = yield* Config.Service
const image = yield* Image.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, image.reload())
yield* image.transform((draft) => {
yield* image.transform((editor) => {
for (const entry of loaded.entries) {
if (entry.type !== "document") continue
const configured = entry.info.media?.image
if (!configured) continue
draft.configure({
editor.configure({
...(configured.auto_resize === undefined ? {} : { autoResize: configured.auto_resize }),
...(configured.max_width === undefined ? {} : { maxWidth: configured.max_width }),
...(configured.max_height === undefined ? {} : { maxHeight: configured.max_height }),
@@ -112,17 +112,17 @@ export const Plugin = define({
Effect.forkScoped({ startImmediately: true }),
)
yield* refresh()
yield* discovery.transform((draft) => {
yield* discovery.transform((editor) => {
if (loaded.current.type === "unavailable") {
draft.unavailable()
editor.unavailable()
return
}
for (const file of loaded.current.files) draft.add(file)
for (const file of loaded.current.files) editor.add(file)
})
}).pipe(
Effect.catchCause((cause) =>
Effect.logWarning("failed to activate instruction source", { cause }).pipe(
Effect.andThen(discovery.transform((draft) => draft.unavailable())),
Effect.andThen(discovery.transform((editor) => editor.unavailable())),
Effect.asVoid,
),
),
@@ -12,10 +12,10 @@ export const Plugin = define({
const config = yield* Config.Service
const policy = yield* LocationWatcherPolicy.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, policy.reload())
yield* policy.transform((draft) => {
yield* policy.transform((editor) => {
for (const entry of loaded.entries) {
if (entry.type !== "document" || !entry.info.watcher?.ignore) continue
draft.add(entry.info.watcher.ignore)
editor.add(entry.info.watcher.ignore)
}
})
}),
+3 -3
View File
@@ -36,7 +36,7 @@ export const register = Effect.fn("ConfigMCPPlugin.register")(function* (
// Subscribe before the initial load so updates racing it trigger a rebuild.
loaded.entries = yield* config.entries()
yield* mcp.transform((draft) => {
yield* mcp.transform((editor) => {
const documents = loaded.entries.filter((entry): entry is Document => entry.type === "document")
// Global timeout defaults merge in config order; each server can override them.
const timeout = Object.assign(
@@ -50,8 +50,8 @@ export const register = Effect.fn("ConfigMCPPlugin.register")(function* (
}
}
for (const [name, server] of servers) {
if (draft.get(name)) continue
draft.set(name, { ...server, timeout: { ...timeout, ...server.timeout } })
if (editor.get(name)) continue
editor.set(name, { ...server, timeout: { ...timeout, ...server.timeout } })
}
})
})
+2 -2
View File
@@ -19,14 +19,14 @@ export const Plugin = define({
const location = yield* Location.Service
const global = yield* Global.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.reference.reload())
yield* ctx.reference.transform((draft) => {
yield* ctx.reference.transform((editor) => {
for (const doc of loaded.entries.filter((entry): entry is Document => entry.type === "document")) {
const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue
const description = typeof entry === "string" ? undefined : entry.description
const hidden = typeof entry === "string" ? undefined : entry.hidden
draft.add(
editor.add(
name,
local(entry)
? Reference.LocalSource.make({
+2 -2
View File
@@ -12,9 +12,9 @@ export const Plugin = define({
const config = yield* Config.Service
const shell = yield* ShellSelect.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, shell.reload())
yield* shell.transform((draft) => {
yield* shell.transform((editor) => {
const configured = Config.latest(loaded.entries, "shell")
if (configured) draft.configure(configured)
if (configured) editor.configure(configured)
})
}),
})
+2 -2
View File
@@ -180,8 +180,8 @@ export const Plugin = define({
Effect.forkScoped({ startImmediately: true }),
)
yield* refresh()
yield* ctx.skill.transform((draft) => {
for (const skill of loaded.skills) draft.add(skill)
yield* ctx.skill.transform((editor) => {
for (const skill of loaded.skills) editor.add(skill)
})
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
+2 -2
View File
@@ -12,10 +12,10 @@ export const Plugin = define({
const config = yield* Config.Service
const snapshot = yield* Snapshot.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, snapshot.reload())
yield* snapshot.transform((draft) => {
yield* snapshot.transform((editor) => {
const configured = Config.latest(loaded.entries, "snapshots")
if (configured === undefined) return
draft.configure(configured)
editor.configure(configured)
})
}),
})
@@ -12,10 +12,10 @@ export const Plugin = define({
const config = yield* Config.Service
const output = yield* ToolOutput.Service
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, output.reload())
yield* output.transform((draft) => {
yield* output.transform((editor) => {
const configured = Config.latest(loaded.entries, "tool_output")
if (!configured) return
draft.configure({
editor.configure({
...(configured.max_lines === undefined ? {} : { maxLines: configured.max_lines }),
...(configured.max_bytes === undefined ? {} : { maxBytes: configured.max_bytes }),
})
@@ -8,12 +8,12 @@ type Data = {
ignore: string[]
}
export type Draft = {
export type Editor = {
add: (ignore: readonly string[]) => void
list: () => readonly string[]
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly current: () => readonly string[]
readonly observe: (
listener: (ignore: readonly string[]) => Effect.Effect<void>,
@@ -26,12 +26,12 @@ const layer = Layer.effect(
Service,
Effect.gen(function* () {
const listeners = new Set<(ignore: readonly string[]) => Effect.Effect<void>>()
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "location-watcher-policy",
initial: () => ({ ignore: [] }),
draft: (draft) => ({
add: (ignore) => draft.ignore.push(...ignore),
list: () => draft.ignore,
editor: (editor) => ({
add: (ignore) => editor.ignore.push(...ignore),
list: () => editor.ignore,
}),
// Read per listener: a reentrant transform inside an earlier listener must reach later ones.
notify: () => Effect.forEach(listeners, (listener) => listener(current()), { discard: true }),
+8 -8
View File
@@ -13,12 +13,12 @@ type Data = {
formatters: Info[]
}
export type Draft = {
export type Editor = {
set: (formatter: Info) => void
remove: (name: string) => void
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly file: (filepath: string) => Effect.Effect<boolean>
}
@@ -30,17 +30,17 @@ const layer = Layer.effect(
const location = yield* Location.Service
const processes = yield* AppProcess.Service
const commands = new WeakMap<Info, string[] | false>()
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "formatter",
initial: () => ({ formatters: [] }),
draft: (draft) => ({
editor: (editor) => ({
set: (formatter) => {
const index = draft.formatters.findIndex((item) => item.name === formatter.name)
if (index === -1) draft.formatters.push(formatter)
else draft.formatters[index] = formatter
const index = editor.formatters.findIndex((item) => item.name === formatter.name)
if (index === -1) editor.formatters.push(formatter)
else editor.formatters[index] = formatter
},
remove: (name) => {
draft.formatters = draft.formatters.filter((formatter) => formatter.name !== name)
editor.formatters = editor.formatters.filter((formatter) => formatter.name !== name)
},
}),
})
+8 -8
View File
@@ -39,11 +39,11 @@ export type Limits = {
maxBase64Bytes: number
}
export type Draft = {
export type Editor = {
configure: (limits: Partial<Limits>) => void
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly normalize: (
resource: string,
content: FileSystem.Content & { readonly encoding: "base64" },
@@ -58,7 +58,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Im
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const state = State.create<Limits, Draft>({
const state = State.create<Limits, Editor>({
name: "image",
initial: () => ({
autoResize: true,
@@ -66,12 +66,12 @@ const layer = Layer.effect(
maxHeight: 2_000,
maxBase64Bytes: 5 * 1024 * 1024,
}),
draft: (draft) => ({
editor: (editor) => ({
configure: (limits) => {
if (limits.autoResize !== undefined) draft.autoResize = limits.autoResize
if (limits.maxWidth !== undefined) draft.maxWidth = limits.maxWidth
if (limits.maxHeight !== undefined) draft.maxHeight = limits.maxHeight
if (limits.maxBase64Bytes !== undefined) draft.maxBase64Bytes = limits.maxBase64Bytes
if (limits.autoResize !== undefined) editor.autoResize = limits.autoResize
if (limits.maxWidth !== undefined) editor.maxWidth = limits.maxWidth
if (limits.maxHeight !== undefined) editor.maxHeight = limits.maxHeight
if (limits.maxBase64Bytes !== undefined) editor.maxBase64Bytes = limits.maxBase64Bytes
},
}),
})
+9 -9
View File
@@ -25,7 +25,7 @@ export type Data = {
available: boolean
}
export type Draft = {
export type Editor = {
list: () => readonly Types.DeepMutable<File>[]
// Map insertion order is render order: config adds global then nearest-to-farthest project files;
// sibling contributors interleave by transform registration order.
@@ -35,7 +35,7 @@ export type Draft = {
unavailable: () => void
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
// Discovery policy lives here because internal plugins have no per-composition options channel.
// Move it into plugin config once plugins can consume their own options.
readonly project: boolean
@@ -57,21 +57,21 @@ export const layer = (options?: Options) =>
Service,
Effect.gen(function* () {
const bus = yield* Bus.Service
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "instruction-discovery",
initial: () => ({ files: new Map(), available: true }),
draft: (draft) => ({
list: () => Array.from(draft.files.values()),
add: (file) => draft.files.set(file.path, new File(file) as Types.DeepMutable<File>),
editor: (editor) => ({
list: () => Array.from(editor.files.values()),
add: (file) => editor.files.set(file.path, new File(file) as Types.DeepMutable<File>),
update: (path, update) => {
const current = draft.files.get(AbsolutePath.make(path))
const current = editor.files.get(AbsolutePath.make(path))
if (!current) return
update(current)
current.path = AbsolutePath.make(path)
},
remove: (path) => draft.files.delete(AbsolutePath.make(path)),
remove: (path) => editor.files.delete(AbsolutePath.make(path)),
unavailable: () => {
draft.available = false
editor.available = false
},
}),
notify: () => bus.publish(Event.Updated, {}).pipe(Effect.asVoid),
+14 -14
View File
@@ -130,7 +130,7 @@ type Data = {
integrations: Map<ID, Entry>
}
export type Draft = {
export type Editor = {
list: () => readonly Ref[]
get: (id: ID) => Ref | undefined
update: (id: ID, update: (integration: Types.DeepMutable<Ref>) => void) => void
@@ -142,7 +142,7 @@ export type Draft = {
}
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
/** Registers a scoped transform over the integration registry. */
/** Returns one integration with its methods and current connections. */
readonly get: (id: ID) => Effect.Effect<Info | undefined>
@@ -266,27 +266,27 @@ const layer = Layer.effect(
const scope = yield* Scope.Scope
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
const commandAttempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, CommandAttemptEntry>())
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "integration",
initial: () => ({ integrations: new Map<ID, Entry>() }),
draft: (draft) => ({
list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
editor: (editor) => ({
list: () => Array.from(editor.integrations.values(), (entry) => entry.ref) as Ref[],
get: (id) => editor.integrations.get(id)?.ref as Ref | undefined,
update: (id, update) => {
const current = draft.integrations.get(id) ?? {
const current = editor.integrations.get(id) ?? {
ref: { id, name: id },
methods: [],
implementations: new Map(),
}
if (!draft.integrations.has(id)) draft.integrations.set(id, current)
if (!editor.integrations.has(id)) editor.integrations.set(id, current)
update(current.ref)
current.ref.id = id
},
remove: (id) => draft.integrations.delete(id),
remove: (id) => editor.integrations.delete(id),
method: {
list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
list: (integrationID) => (editor.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
update: (implementation) => {
const current = draft.integrations.get(implementation.integrationID) ?? {
const current = editor.integrations.get(implementation.integrationID) ?? {
ref: {
id: implementation.integrationID,
name: implementation.integrationID,
@@ -294,8 +294,8 @@ const layer = Layer.effect(
methods: [],
implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
}
if (!draft.integrations.has(implementation.integrationID)) {
draft.integrations.set(implementation.integrationID, current)
if (!editor.integrations.has(implementation.integrationID)) {
editor.integrations.set(implementation.integrationID, current)
}
const index = current.methods.findIndex((method) => {
if (method.type !== implementation.method.type) return false
@@ -315,7 +315,7 @@ const layer = Layer.effect(
}
},
remove: (integrationID, method) => {
const current = draft.integrations.get(integrationID)
const current = editor.integrations.get(integrationID)
if (!current) return
const index = current.methods.findIndex((candidate) => {
if (candidate.type !== method.type) return false
+13 -13
View File
@@ -133,7 +133,7 @@ type Data = {
removed: Set<ServerName>
}
export type Draft = {
export type Editor = {
list: () => readonly [ServerName, Types.DeepMutable<Mcp.ServerConfig>][]
get: (server: ServerName | string) => Types.DeepMutable<Mcp.ServerConfig> | undefined
set: (server: ServerName | string, config: Mcp.ServerConfig) => void
@@ -143,7 +143,7 @@ export type Draft = {
const cloneConfig = (config: Mcp.ServerConfig) => structuredClone(config) as Types.DeepMutable<Mcp.ServerConfig>
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly servers: () => Effect.Effect<ServerInfo[]>
readonly add: (server: ServerName | string, config: Mcp.ServerConfig) => Effect.Effect<void>
readonly connect: (server: ServerName | string) => Effect.Effect<void, NotFoundError>
@@ -227,12 +227,12 @@ export const layer = (options?: Options) =>
const scope = yield* Scope.fork(root)
entry.registration = { dispose: Scope.close(scope, Exit.void) }
yield* integration
.transform((draft) => {
draft.update(integrationID, (ref) => {
.transform((editor) => {
editor.update(integrationID, (ref) => {
ref.name = name
ref.metadata = { source: "mcp" }
})
draft.method.update({
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: name },
authorize: () =>
@@ -678,7 +678,7 @@ export const layer = (options?: Options) =>
Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))),
),
)
const state: State.Interface<Data, Draft> = State.create<Data, Draft>({
const state: State.Interface<Data, Editor> = State.create<Data, Editor>({
name: "mcp",
initial: () => ({
servers: new Map(
@@ -688,20 +688,20 @@ export const layer = (options?: Options) =>
),
removed: new Set(Array.from(overrides).flatMap(([name, config]) => (config === false ? [name] : []))),
}),
draft: (draft) => ({
list: () => Array.from(draft.servers),
get: (server) => draft.servers.get(ServerName.make(server)),
editor: (editor) => ({
list: () => Array.from(editor.servers),
get: (server) => editor.servers.get(ServerName.make(server)),
set: (server, serverConfig) => {
const name = ServerName.make(server)
if (draft.removed.has(name)) return
draft.servers.set(name, cloneConfig(serverConfig))
if (editor.removed.has(name)) return
editor.servers.set(name, cloneConfig(serverConfig))
},
update: (server, update) => {
const current = draft.servers.get(ServerName.make(server))
const current = editor.servers.get(ServerName.make(server))
if (!current) return
update(current)
},
remove: (server) => draft.servers.delete(ServerName.make(server)),
remove: (server) => editor.servers.delete(ServerName.make(server)),
}),
notify: () => State.reconcile(root, fork, () => reconcileLock.withPermit(reconcile())),
})
+7 -7
View File
@@ -83,15 +83,15 @@ Rules:
export const Plugin = define({
id: "opencode.agent",
effect: Effect.fn(function* (ctx) {
yield* ctx.agent.transform((draft) => {
draft.update(Agent.defaultID, (item) => {
yield* ctx.agent.transform((editor) => {
editor.update(Agent.defaultID, (item) => {
item.name = Agent.Name.make("Build")
item.description = "The default agent. Executes tools based on configured permissions."
item.mode = "primary"
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
})
draft.update(Agent.ID.make("general"), (item) => {
editor.update(Agent.ID.make("general"), (item) => {
item.name = Agent.Name.make("General")
item.description =
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
@@ -102,7 +102,7 @@ export const Plugin = define({
)
})
draft.update(Agent.ID.make("explore"), (item) => {
editor.update(Agent.ID.make("explore"), (item) => {
const externalDirectories = item.permissions.filter(
(rule) => rule.action === "external_directory" && rule.effect === "allow",
)
@@ -130,13 +130,13 @@ export const Plugin = define({
)
})
draft.update(Agent.ID.make("compaction"), (item) => {
editor.update(Agent.ID.make("compaction"), (item) => {
item.name = Agent.Name.make("Compaction")
item.mode = "primary"
item.hidden = true
})
draft.update(Agent.ID.make("title"), (item) => {
editor.update(Agent.ID.make("title"), (item) => {
item.name = Agent.Name.make("Title")
item.mode = "primary"
item.hidden = true
@@ -144,7 +144,7 @@ export const Plugin = define({
item.permissions.push({ action: "*", resource: "*", effect: "deny" })
})
draft.update(Agent.ID.make("summary"), (item) => {
editor.update(Agent.ID.make("summary"), (item) => {
item.name = Agent.Name.make("Summary")
item.mode = "primary"
item.hidden = true
+4 -4
View File
@@ -25,8 +25,8 @@ export const Plugin = define({
Effect.forkScoped({ startImmediately: true }),
)
loaded.prompts = yield* mcp.prompts()
yield* ctx.command.transform((draft) => {
draft.add({
yield* ctx.command.transform((editor) => {
editor.add({
name: "init",
description: "guided AGENTS.md setup",
execute: (input) =>
@@ -39,7 +39,7 @@ export const Plugin = define({
})
.pipe(Effect.asVoid),
})
draft.add({
editor.add({
name: "review",
description: "review changes [commit|branch|pr], defaults to uncommitted",
execute: (input) =>
@@ -53,7 +53,7 @@ export const Plugin = define({
.pipe(Effect.asVoid),
})
for (const prompt of loaded.prompts) {
draft.add({
editor.add({
name: mcpCommandName(prompt.server, prompt.name),
description: prompt.description,
execute: (input) =>
+44 -43
View File
@@ -136,13 +136,13 @@ export const make = Effect.fn("PluginHost.make")(function* (
},
reload: agents.reload,
transform: (callback) =>
agents.transform((draft) => {
agents.transform((editor) => {
callback({
list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(Agent.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)),
update: (id, update) => draft.update(Agent.ID.make(id), update),
remove: (id) => draft.remove(Agent.ID.make(id)),
list: () => mutable(editor.list()),
get: (id) => mutable(editor.get(Agent.ID.make(id))),
default: (id) => editor.default(id === undefined ? undefined : Agent.ID.make(id)),
update: (id, update) => editor.update(Agent.ID.make(id), update),
remove: (id) => editor.remove(Agent.ID.make(id)),
})
}),
},
@@ -196,24 +196,25 @@ export const make = Effect.fn("PluginHost.make")(function* (
},
reload: catalog.reload,
transform: (callback) =>
catalog.transform((draft) => {
catalog.transform((editor) => {
callback({
provider: {
list: () => mutable(draft.provider.list()),
get: (id) => mutable(draft.provider.get(Provider.ID.make(id))),
update: (id, update) => draft.provider.update(Provider.ID.make(id), update),
remove: (id) => draft.provider.remove(Provider.ID.make(id)),
list: () => mutable(editor.provider.list()),
get: (id) => mutable(editor.provider.get(Provider.ID.make(id))),
update: (id, update) => editor.provider.update(Provider.ID.make(id), update),
remove: (id) => editor.provider.remove(Provider.ID.make(id)),
},
model: {
get: (providerID, modelID) =>
mutable(draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))),
mutable(editor.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))),
update: (providerID, modelID, update) =>
draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), update),
remove: (providerID, modelID) => draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)),
editor.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), update),
remove: (providerID, modelID) =>
editor.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)),
default: {
get: draft.model.default.get,
get: editor.model.default.get,
set: (providerID, modelID) =>
draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)),
editor.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)),
},
},
})
@@ -315,17 +316,17 @@ export const make = Effect.fn("PluginHost.make")(function* (
),
},
transform: (callback) =>
integration.transform((draft) => {
integration.transform((editor) => {
callback({
list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(Integration.ID.make(id))),
update: (id, update) => draft.update(Integration.ID.make(id), update),
remove: (id) => draft.remove(Integration.ID.make(id)),
list: () => mutable(editor.list()),
get: (id) => mutable(editor.get(Integration.ID.make(id))),
update: (id, update) => editor.update(Integration.ID.make(id), update),
remove: (id) => editor.remove(Integration.ID.make(id)),
method: {
list: (id) => draft.method.list(Integration.ID.make(id)),
update: (input) => draft.method.update(methodImplementation(input)),
list: (id) => editor.method.list(Integration.ID.make(id)),
update: (input) => editor.method.update(methodImplementation(input)),
remove: (id, method) =>
draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
editor.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
},
})
}),
@@ -350,13 +351,13 @@ export const make = Effect.fn("PluginHost.make")(function* (
},
reload: mcp.reload,
transform: (callback) =>
mcp.transform((draft) => {
mcp.transform((editor) => {
callback({
list: () => draft.list().map(([name, config]) => [name, mutable(config)]),
get: (name) => mutable(draft.get(name)),
set: (name, config) => draft.set(name, Schema.decodeUnknownSync(ServerConfig)(config)),
update: draft.update,
remove: draft.remove,
list: () => editor.list().map(([name, config]) => [name, mutable(config)]),
get: (name) => mutable(editor.get(name)),
set: (name, config) => editor.set(name, Schema.decodeUnknownSync(ServerConfig)(config)),
update: editor.update,
remove: editor.remove,
})
}),
},
@@ -391,11 +392,11 @@ export const make = Effect.fn("PluginHost.make")(function* (
list: () => response(reference.list()),
reload: reference.reload,
transform: (callback) =>
reference.transform((draft) => {
reference.transform((editor) => {
callback({
add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
remove: draft.remove,
list: draft.list,
add: (name, source) => editor.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
remove: editor.remove,
list: editor.list,
})
}),
},
@@ -403,12 +404,12 @@ export const make = Effect.fn("PluginHost.make")(function* (
list: () => response(skill.list()),
reload: skill.reload,
transform: (callback) =>
skill.transform((draft) => {
skill.transform((editor) => {
callback({
list: () => mutable(draft.list()),
add: (value) => draft.add(Schema.decodeUnknownSync(Skill.Info)(value)),
update: draft.update,
remove: draft.remove,
list: () => mutable(editor.list()),
add: (value) => editor.add(Schema.decodeUnknownSync(Skill.Info)(value)),
update: editor.update,
remove: editor.remove,
})
}),
},
@@ -441,18 +442,18 @@ export const make = Effect.fn("PluginHost.make")(function* (
),
reload: websearch.reload,
transform: (callback) =>
websearch.transform((draft) => {
websearch.transform((editor) => {
callback({
add: (definition) =>
draft.add({
editor.add({
id: WebSearch.ID.make(definition.id),
name: definition.name,
execute: definition.execute,
}),
default: {
get: draft.default.get,
get: editor.default.get,
set: (selection) =>
draft.default.set(
editor.default.set(
selection === false || selection === "random" ? selection : WebSearch.ID.make(selection),
),
},
@@ -9,8 +9,8 @@ const urls = [/^https:\/\/executor\.sh\/[^/]+\/mcp$/]
export const Plugin = define({
id: "opencode.mcp.codemode.exclusion",
effect: Effect.fn(function* (ctx) {
yield* ctx.mcp.transform((draft) => {
for (const [, server] of draft.list()) {
yield* ctx.mcp.transform((editor) => {
for (const [, server] of editor.list()) {
if (server.codemode !== undefined) continue
if (server.type === "local") {
if (server.command[0] === "executor" && server.command[1] === "mcp") server.codemode = false
+2 -2
View File
@@ -30,8 +30,8 @@ export const Plugin = define({
const global = yield* Global.Service
const directory = path.join(global.home, ".opencode", "plan")
const enterReminder = enter(directory)
yield* ctx.agent.transform((draft) => {
draft.update(plan, (item) => {
yield* ctx.agent.transform((editor) => {
editor.update(plan, (item) => {
item.name = Agent.Name.make("Plan")
item.description = "Read-only agent for exploring the codebase and planning work before implementation."
item.mode = "primary"
+3 -3
View File
@@ -80,13 +80,13 @@ export const AzurePlugin = define({
])
})
yield* ctx.integration.transform((draft) => {
draft.method.update({
yield* ctx.integration.transform((editor) => {
editor.method.update({
integrationID: Provider.ID.azure,
method: { type: "key", label: "API key", form: form() },
})
if (!available) return
draft.method.update({
editor.method.update({
integrationID: Provider.ID.azure,
method: {
id: methodID,
@@ -39,8 +39,8 @@ export const CloudflareAIGatewayPlugin = define({
if (gatewayId) return Form.Fields.make([accountIdForm])
return Form.Fields.make([accountIdForm, gatewayIdForm])
})
yield* ctx.integration.transform((draft) => {
draft.method.update({
yield* ctx.integration.transform((editor) => {
editor.method.update({
integrationID: providerID,
method: {
type: "key",
@@ -25,8 +25,8 @@ export const CloudflareWorkersAIPlugin = define({
},
])
})
yield* ctx.integration.transform((draft) => {
draft.method.update({
yield* ctx.integration.transform((editor) => {
editor.method.update({
integrationID: providerID,
method: {
type: "key",
@@ -190,9 +190,9 @@ export const GithubCopilotPlugin = define({
)
})
yield* ctx.integration.transform((draft) => {
draft.method.remove("github-copilot", { type: "key" })
draft.method.update(oauth(ctx.app))
yield* ctx.integration.transform((editor) => {
editor.method.remove("github-copilot", { type: "key" })
editor.method.update(oauth(ctx.app))
})
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(Provider.ID.githubCopilot)
+3 -3
View File
@@ -245,9 +245,9 @@ export const OpenAIPlugin = define({
: undefined
})
yield* ctx.integration.transform((draft) => {
draft.method.update(browser(ctx.app))
draft.method.update(headless(ctx.app))
yield* ctx.integration.transform((editor) => {
editor.method.update(browser(ctx.app))
editor.method.update(headless(ctx.app))
})
yield* load()
yield* ctx.catalog.transform((evt) => {
@@ -103,12 +103,12 @@ export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope
: undefined
})
yield* ctx.integration.transform((draft) => {
draft.update("opencode", (integration) => {
yield* ctx.integration.transform((editor) => {
editor.update("opencode", (integration) => {
integration.name = "OpenCode"
})
draft.method.update(oauth(http))
draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } })
editor.method.update(oauth(http))
editor.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } })
})
yield* load()
+4 -4
View File
@@ -88,12 +88,12 @@ export const XAIPlugin = define({
{ discard: true },
)
yield* ctx.integration.transform((draft) => {
draft.update("xai", (integration) => {
yield* ctx.integration.transform((editor) => {
editor.update("xai", (integration) => {
integration.name = "xAI"
})
draft.method.update(device(ctx.app))
draft.method.update({ integrationID: "xai", method: { type: "key", label: "Manually enter API Key" } })
editor.method.update(device(ctx.app))
editor.method.update({ integrationID: "xai", method: { type: "key", label: "Manually enter API Key" } })
})
yield* ctx.catalog.transform((catalog) => {
const provider = catalog.provider.get(providerID)
+3 -3
View File
@@ -24,8 +24,8 @@ export const Plugin = define({
id: "opencode.skill",
effect: Effect.fn(function* (ctx) {
const reportContent = yield* reportContentWithDiagnostics(ctx.app)
yield* ctx.skill.transform((draft) => {
draft.add(
yield* ctx.skill.transform((editor) => {
editor.add(
Skill.Info.make({
id: Skill.ID.make("opencode"),
name: Skill.Name.make("OpenCode"),
@@ -34,7 +34,7 @@ export const Plugin = define({
content: OpencodeContent,
}),
)
draft.add(
editor.add(
Skill.Info.make({
id: Skill.ID.make("report"),
name: Skill.Name.make("Report"),
+2 -2
View File
@@ -30,8 +30,8 @@ export const Plugin = define({
worktree: location.project.directory,
})
yield* ctx.vcs.transform((draft) => {
draft.add({
yield* ctx.vcs.transform((editor) => {
editor.add({
id: "git",
name: "Git",
info: () => adapter.info(),
+2 -2
View File
@@ -35,8 +35,8 @@ export const Plugin = define({
worktree: location.project.directory,
})
yield* ctx.vcs.transform((draft) => {
draft.add({
yield* ctx.vcs.transform((editor) => {
editor.add({
id: "hg",
name: "Mercurial",
info: () => adapter.info(),
+6 -6
View File
@@ -26,19 +26,19 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
id: "opencode.websearch.exa",
effect: Effect.fn("WebSearchExa.Plugin")(function* (ctx) {
const http = yield* HttpClient.HttpClient
yield* ctx.integration.transform((draft) => {
draft.update("exa", (integration) => (integration.name = "Exa"))
draft.method.update({
yield* ctx.integration.transform((editor) => {
editor.update("exa", (integration) => (integration.name = "Exa"))
editor.method.update({
integrationID: "exa",
method: { type: "key" },
})
draft.method.update({
editor.method.update({
integrationID: "exa",
method: { type: "env", names: ["EXA_API_KEY"] },
})
})
yield* ctx.websearch.transform((draft) => {
draft.add({
yield* ctx.websearch.transform((editor) => {
editor.add({
id: "exa",
name: "Exa",
execute: (input) =>
@@ -37,19 +37,19 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
id: "opencode.websearch.firecrawl",
effect: Effect.fn("WebSearchFirecrawl.Plugin")(function* (ctx) {
const http = yield* HttpClient.HttpClient
yield* ctx.integration.transform((draft) => {
draft.update("firecrawl", (integration) => (integration.name = "Firecrawl"))
draft.method.update({
yield* ctx.integration.transform((editor) => {
editor.update("firecrawl", (integration) => (integration.name = "Firecrawl"))
editor.method.update({
integrationID: "firecrawl",
method: { type: "key" },
})
draft.method.update({
editor.method.update({
integrationID: "firecrawl",
method: { type: "env", names: ["FIRECRAWL_API_KEY"] },
})
})
yield* ctx.websearch.transform((draft) => {
draft.add({
yield* ctx.websearch.transform((editor) => {
editor.add({
id: "firecrawl",
name: "Firecrawl",
execute: (input) =>
@@ -52,19 +52,19 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
id: "opencode.websearch.parallel",
effect: Effect.fn("WebSearchParallel.Plugin")(function* (ctx) {
const http = yield* HttpClient.HttpClient
yield* ctx.integration.transform((draft) => {
draft.update("parallel", (integration) => (integration.name = "Parallel"))
draft.method.update({
yield* ctx.integration.transform((editor) => {
editor.update("parallel", (integration) => (integration.name = "Parallel"))
editor.method.update({
integrationID: "parallel",
method: { type: "key" },
})
draft.method.update({
editor.method.update({
integrationID: "parallel",
method: { type: "env", names: ["PARALLEL_API_KEY"] },
})
})
yield* ctx.websearch.transform((draft) => {
draft.add({
yield* ctx.websearch.transform((editor) => {
editor.add({
id: "parallel",
name: "Parallel",
execute: (input) =>
+6 -6
View File
@@ -28,19 +28,19 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
id: "opencode.websearch.tavily",
effect: Effect.fn("WebSearchTavily.Plugin")(function* (ctx) {
const http = yield* HttpClient.HttpClient
yield* ctx.integration.transform((draft) => {
draft.update("tavily", (integration) => (integration.name = "Tavily"))
draft.method.update({
yield* ctx.integration.transform((editor) => {
editor.update("tavily", (integration) => (integration.name = "Tavily"))
editor.method.update({
integrationID: "tavily",
method: { type: "key" },
})
draft.method.update({
editor.method.update({
integrationID: "tavily",
method: { type: "env", names: ["TAVILY_API_KEY"] },
})
})
yield* ctx.websearch.transform((draft) => {
draft.add({
yield* ctx.websearch.transform((editor) => {
editor.add({
id: "tavily",
name: "Tavily",
execute: (input) =>
+7 -7
View File
@@ -28,13 +28,13 @@ type Data = {
sources: Map<string, Source>
}
type Draft = {
type Editor = {
add(name: string, source: Source): void
remove(name: string): void
list(): readonly [string, Source][]
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly list: () => Effect.Effect<Info[]>
}
@@ -91,13 +91,13 @@ const layer = Layer.effect(
{ concurrency: 4, discard: true },
)
})
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "reference",
initial: () => ({ sources: new Map() }),
draft: (draft) => ({
add: (name, source) => draft.sources.set(name, source),
remove: (name) => draft.sources.delete(name),
list: () => Array.from(draft.sources),
editor: (editor) => ({
add: (name, source) => editor.sources.set(name, source),
remove: (name) => editor.sources.delete(name),
list: () => Array.from(editor.sources),
}),
notify: () =>
Effect.gen(function* () {
+7 -7
View File
@@ -75,7 +75,7 @@ export type Settings = {
tokens: number
}
export type Draft = {
export type Editor = {
configure: (settings: Partial<Settings>) => void
}
@@ -116,7 +116,7 @@ export type Outcome =
| Pick<SessionMessage.CompactionCompleted, "status">
| Pick<SessionMessage.CompactionFailed, "status" | "error">
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly enabled: () => boolean
readonly required: (input: RequiredInput) => boolean
readonly compact: (input: AutoInput) => Effect.Effect<Outcome>
@@ -326,14 +326,14 @@ export const layer = Layer.effect(
const bus = yield* Bus.Service
const llm = yield* LLMClient.Service
const state = State.create<Settings, Draft>({
const state = State.create<Settings, Editor>({
name: "session-compaction",
initial: () => ({ auto: true, buffer: DEFAULT_BUFFER, tokens: DEFAULT_KEEP_TOKENS }),
draft: (draft) => ({
editor: (editor) => ({
configure: (settings) => {
if (settings.auto !== undefined) draft.auto = settings.auto
if (settings.buffer !== undefined) draft.buffer = settings.buffer
if (settings.tokens !== undefined) draft.tokens = settings.tokens
if (settings.auto !== undefined) editor.auto = settings.auto
if (settings.buffer !== undefined) editor.buffer = settings.buffer
if (settings.tokens !== undefined) editor.tokens = settings.tokens
},
}),
})
+5 -5
View File
@@ -37,7 +37,7 @@ type Data = {
shell?: string
}
export type Draft = {
export type Editor = {
configure: (shell: string) => void
}
@@ -45,7 +45,7 @@ export type ResolveInput = {
priority: "config" | "compat"
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly resolve: (input: ResolveInput) => Effect.Effect<string>
}
@@ -201,12 +201,12 @@ const layer = (options?: Options) =>
Service,
Effect.gen(function* () {
const global = yield* Global.Service
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "shell-select",
initial: () => ({}),
draft: (draft) => ({
editor: (editor) => ({
configure: (shell) => {
draft.shell = shell
editor.shell = shell
},
}),
})
+8 -8
View File
@@ -72,14 +72,14 @@ export type Data = {
skills: Map<ID, Types.DeepMutable<Info>>
}
export type Draft = {
export type Editor = {
list: () => readonly Types.DeepMutable<Info>[]
add: (skill: Info) => void
update: (id: string, update: (skill: Types.DeepMutable<Info>) => void) => void
remove: (id: string) => void
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly get: (id: ID) => Effect.Effect<Info | undefined>
readonly list: () => Effect.Effect<Info[]>
}
@@ -91,22 +91,22 @@ const layer = Layer.effect(
Effect.gen(function* () {
const bus = yield* Bus.Service
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "skill",
initial: () => ({ skills: new Map() }),
draft: (draft) => ({
list: () => Array.from(draft.skills.values()),
editor: (editor) => ({
list: () => Array.from(editor.skills.values()),
add: (skill) => {
draft.skills.set(skill.id, { ...skill } as Types.DeepMutable<Info>)
editor.skills.set(skill.id, { ...skill } as Types.DeepMutable<Info>)
},
update: (id, update) => {
const current = draft.skills.get(ID.make(id))
const current = editor.skills.get(ID.make(id))
if (!current) return
update(current)
current.id = ID.make(id)
},
remove: (id) => {
draft.skills.delete(ID.make(id))
editor.skills.delete(ID.make(id))
},
}),
notify: () => bus.publish(Skill.Event.Updated, {}).pipe(Effect.asVoid),
+5 -5
View File
@@ -36,11 +36,11 @@ export interface RestoreInput {
readonly files: ReadonlyMap<RelativePath, ID>
}
export type Draft = {
export type Editor = {
configure: (enabled: boolean) => void
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
/**
* Capture the current Location-scoped filesystem state as a content-addressed
* tree. Returns `undefined` when snapshots are disabled, unsupported, or the
@@ -77,12 +77,12 @@ const layer = Layer.effect(
const global = yield* Global.Service
const location = yield* Location.Service
const lifetime = yield* Scope.Scope
const state = State.create<{ enabled: boolean }, Draft>({
const state = State.create<{ enabled: boolean }, Editor>({
name: "snapshot",
initial: () => ({ enabled: true }),
draft: (draft) => ({
editor: (editor) => ({
configure: (enabled) => {
draft.enabled = enabled
editor.enabled = enabled
},
}),
})
+15 -15
View File
@@ -5,11 +5,11 @@ import { Cause, Context, Effect, Exit, Fiber, Scope } from "effect"
/**
* A synchronous, replayable edit to the current domain state.
*
* Domain drafts expose readable and writable state while preserving concise
* Domain editors expose readable and writable state while preserving concise
* plugin/config code. Transforms synchronously rebuild derived state.
*/
type TransformCallback<DraftApi> = (draft: DraftApi) => void
export type MakeDraft<State, DraftApi> = (state: State) => DraftApi
type TransformCallback<Editor> = (editor: Editor) => void
export type MakeEditor<State, Editor> = (state: State) => Editor
export interface Registration {
readonly dispose: Effect.Effect<void>
@@ -19,15 +19,15 @@ export interface Registration {
* Registers a scoped transform. Reads rebuild by applying every registered transform in order.
* Closing the owning Scope removes the transform and invalidates the current value.
*/
export type Transform<DraftApi> = (
transform: TransformCallback<DraftApi>,
export type Transform<Editor> = (
transform: TransformCallback<Editor>,
) => Effect.Effect<Registration, never, Scope.Scope>
/** Invalidates the current value after captured inputs change and notifies like a registration would. */
export type Reload = () => Effect.Effect<void>
export interface Transformable<DraftApi> {
readonly transform: Transform<DraftApi>
export interface Transformable<Editor> {
readonly transform: Transform<Editor>
readonly reload: Reload
}
@@ -88,12 +88,12 @@ export const inherit = Effect.fnUntraced(function* () {
return <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.provideService(effect, CurrentBatch, batch)
})
export interface Options<State, DraftApi> {
export interface Options<State, Editor> {
readonly name?: string
/** Creates the empty base value for every rebuild. */
readonly initial: () => State
/** Wraps mutable state in a domain-specific draft API. */
readonly draft: MakeDraft<State, DraftApi>
/** Wraps mutable state in a domain-specific editor API. */
readonly editor: MakeEditor<State, Editor>
/**
* Observes the freshly rebuilt value outside the read path. Every registration, disposal, or
* reload notifies once it is applied; a batch coalesces them into one notification at its end.
@@ -102,7 +102,7 @@ export interface Options<State, DraftApi> {
readonly notify?: (state: State) => Effect.Effect<void>
}
export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
export interface Interface<State, Editor> extends Transformable<Editor> {
/**
* Rebuilds synchronously when transforms changed since the last read. Each rebuild produces a new
* value and never touches earlier ones, so callers may retain what they read.
@@ -110,17 +110,17 @@ export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
readonly get: () => State
}
export function create<State, DraftApi>(options: Options<State, DraftApi>): Interface<State, DraftApi> {
export function create<State, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
let state = options.initial()
const transforms: { run: TransformCallback<DraftApi> }[] = []
const transforms: { run: TransformCallback<Editor> }[] = []
let dirty = false
let closed = false
const get = () => {
if (closed || !dirty) return state
const next = options.initial()
const draft = options.draft(next)
for (const transform of transforms) transform.run(draft)
const editor = options.editor(next)
for (const transform of transforms) transform.run(editor)
// Only a complete fold becomes visible; a throwing callback leaves the previous value and stays dirty.
state = next
dirty = false
+6 -6
View File
@@ -22,11 +22,11 @@ type Limits = {
maxBytes: number
}
export type Draft = {
export type Editor = {
configure: (limits: Partial<Limits>) => void
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly truncate: (result: Result) => Effect.Effect<Result>
readonly cleanup: () => Effect.Effect<void>
}
@@ -51,13 +51,13 @@ const layer = Layer.effect(
const fs = yield* FSUtil.Service
const global = yield* Global.Service
const directory = path.join(global.data, DIRECTORY)
const state = State.create<Limits, Draft>({
const state = State.create<Limits, Editor>({
name: "tool-output",
initial: () => ({ maxLines: MAX_LINES, maxBytes: MAX_BYTES }),
draft: (draft) => ({
editor: (editor) => ({
configure: (limits) => {
if (limits.maxLines !== undefined) draft.maxLines = limits.maxLines
if (limits.maxBytes !== undefined) draft.maxBytes = limits.maxBytes
if (limits.maxLines !== undefined) editor.maxLines = limits.maxLines
if (limits.maxBytes !== undefined) editor.maxBytes = limits.maxBytes
},
}),
})
+14 -14
View File
@@ -23,7 +23,7 @@ export class RegistrationError extends Schema.TaggedError<RegistrationError>()("
message: Schema.String,
}) {}
export interface Draft {
export interface Editor {
readonly list: () => readonly (Tool.Info & { readonly id: string })[]
readonly get: (id: string) => (Tool.Info & { readonly id: string }) | undefined
readonly namespace: (namespace: Tool.Namespace) => void
@@ -38,7 +38,7 @@ type Data = {
errors: { kind: "tool" | "namespace"; name: string; namespace?: string; error: RegistrationError }[]
}
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly snapshot: (permissions?: Permission.Ruleset) => Effect.Effect<Snapshot>
}
@@ -149,35 +149,35 @@ const layer = Layer.effect(
}
})
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
name: "tool",
initial: () => ({
tools: new Map(),
namespaces: new Map(),
errors: [],
}),
draft: (draft) => ({
list: () => Array.from(draft.tools.values()),
get: (id) => draft.tools.get(id),
editor: (editor) => ({
list: () => Array.from(editor.tools.values()),
get: (id) => editor.tools.get(id),
namespace: (namespace) => {
const error = namespaceError(namespace.name)
if (error) {
draft.errors.push({ kind: "namespace", name: namespace.name, namespace: namespace.name, error })
editor.errors.push({ kind: "namespace", name: namespace.name, namespace: namespace.name, error })
return
}
draft.namespaces.set(namespace.name, { ...namespace })
editor.namespaces.set(namespace.name, { ...namespace })
},
add: (tool) => {
const error = registrationError(tool)
if (error) {
draft.errors.push({ kind: "tool", name: tool.name, namespace: tool.options?.namespace, error })
editor.errors.push({ kind: "tool", name: tool.name, namespace: tool.options?.namespace, error })
return
}
const id = effectiveName(tool)
draft.tools.set(id, { ...tool, id, options: tool.options && { ...tool.options } })
editor.tools.set(id, { ...tool, id, options: tool.options && { ...tool.options } })
},
update: (id, update) => {
const current = draft.tools.get(id)
const current = editor.tools.get(id)
if (!current) return
const tool = { ...current, options: current.options && { ...current.options } }
update(tool)
@@ -187,13 +187,13 @@ const layer = Layer.effect(
tool.options = { ...tool.options, namespace: current.options?.namespace }
const error = registrationError(tool)
if (error) {
draft.errors.push({ kind: "tool", name: tool.name, namespace: tool.options?.namespace, error })
editor.errors.push({ kind: "tool", name: tool.name, namespace: tool.options?.namespace, error })
return
}
draft.tools.set(id, tool)
editor.tools.set(id, tool)
},
remove: (id) => {
draft.tools.delete(id)
editor.tools.delete(id)
},
}),
notify: (value) =>
+4 -4
View File
@@ -4,7 +4,7 @@
## Representations
- Plugin authors get schema-derived input types at the `ToolDraft.add` boundary through `Tool`.
- Plugin authors get schema-derived input types at the `ToolEditor.add` boundary through `Tool`.
- The heterogeneous Core registry deliberately erases registered definitions to `Tool.Info`. Use `any` at this internal boundary; do not replace it with `unknown`, JSON-value plumbing, casts, or compiled wrapper types solely to preserve type safety after registration.
- Executors return model content and metadata alongside declared machine output. Shipped built-ins and plugin tools use the same runtime shape after registration.
- `src/tool.ts` stores canonical Location registrations, derives LLM definitions, executes tools, and normalizes model content and images.
@@ -30,11 +30,11 @@ Leaves own resolution, permission, and side-effect ordering. Translate only expe
## Registration
Built-ins, plugins, and MCP install tools through `Tool.Service.transform`, adding complete tool objects to the draft. A tool may provide a namespace, which flattens direct model names to `<namespace>_<tool>`, and defaults into CodeMode (`codemode` defaults true; `codemode: false` keeps the tool on the provider's native tool list).
Built-ins, plugins, and MCP install tools through `Tool.Service.transform`, adding complete tool objects to the editor. A tool may provide a namespace, which flattens direct model names to `<namespace>_<tool>`, and defaults into CodeMode (`codemode` defaults true; `codemode: false` keeps the tool on the provider's native tool list).
Namespace descriptions are registered once through `draft.namespace(...)`. Tool options continue to reference the namespace by string name; an unregistered namespace remains valid and simply has no namespace description.
Namespace descriptions are registered once through `editor.namespace(...)`. Tool options continue to reference the namespace by string name; an unregistered namespace remains valid and simply has no namespace description.
The service uses shared `State` to replay synchronous transforms in registration order against a fresh draft. `Tool.Service.reload()` rebuilds from captured source data without changing registration precedence. Registrations are scoped and return a real, idempotent `dispose` Effect:
The service uses shared `State` to replay synchronous transforms in registration order against a fresh editor. `Tool.Service.reload()` rebuilds from captured source data without changing registration precedence. Registrations are scoped and return a real, idempotent `dispose` Effect:
- The latest valid active registration for the same effective name wins.
- `update` and `remove` target effective names and do nothing for missing tools. Updates preserve the name and namespace; invalid updates leave the previous definition intact. Creating a tool requires `add`.
+2 -2
View File
@@ -38,10 +38,10 @@ export const layer = Layer.effect(
.withPermit(
Effect.gen(function* () {
discovered = yield* mcp.tools()
yield* tools.transform((draft) => {
yield* tools.transform((editor) => {
for (const tool of discovered) {
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
draft.add({
editor.add({
name: tool.name,
options: { namespace: namespace(tool.server), codemode: tool.codemode !== false },
description: tool.description ?? "",
+2 -2
View File
@@ -117,8 +117,8 @@ export const Plugin = {
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false, permission: "edit" },
description:
+2 -2
View File
@@ -52,8 +52,8 @@ export const Plugin = {
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description: 'Search file paths using a glob pattern (examples: "**/*.ts", "src/**/*.tsx").',
+2 -2
View File
@@ -71,8 +71,8 @@ export const Plugin = {
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description:
+2 -2
View File
@@ -74,8 +74,8 @@ export const Plugin = {
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false, permission: "edit" },
description: DESCRIPTION,
+2 -2
View File
@@ -52,8 +52,8 @@ export const Plugin = {
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description,
+2 -2
View File
@@ -38,8 +38,8 @@ export const Plugin = {
const location = yield* Location.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description:
+2 -2
View File
@@ -196,8 +196,8 @@ export const Plugin = {
)
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description: description(),
+2 -2
View File
@@ -36,8 +36,8 @@ export const Plugin = {
const skills = yield* Skill.Service
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description,
+2 -2
View File
@@ -98,8 +98,8 @@ export const Plugin = {
})
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description,
+2 -2
View File
@@ -107,8 +107,8 @@ export const Plugin = {
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description,
+2 -2
View File
@@ -32,8 +32,8 @@ export const Plugin = {
const websearch = yield* WebSearch.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false },
description,
+2 -2
View File
@@ -53,8 +53,8 @@ export const Plugin = {
const permission = yield* Permission.Service
yield* ctx.tool
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name,
options: { codemode: false, permission: "edit" },
description:
+7 -7
View File
@@ -2,7 +2,7 @@ export * as Vcs from "./vcs.js"
import path from "path"
import { Cause, Context, Effect, FiberSet, Layer, Schema, Semaphore, Stream } from "effect"
import type { VcsDefinition, VcsDraft } from "@opencode-ai/plugin/effect/vcs"
import type { VcsDefinition, VcsEditor } from "@opencode-ai/plugin/effect/vcs"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Base, BranchList, FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
@@ -38,7 +38,7 @@ export interface Adapter {
readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect<FileDiff.Info[], DiffError>
}
export interface Interface extends Adapter, State.Transformable<VcsDraft> {
export interface Interface extends Adapter, State.Transformable<VcsEditor> {
readonly base: () => Effect.Effect<Base | null, DiffError>
}
@@ -71,14 +71,14 @@ const layer = Layer.effect(
const decodeBranches = Schema.decodeUnknownEffect(BranchList)
const decodeStatus = Schema.decodeUnknownEffect(Schema.Array(FileStatus))
const decodeDiff = Schema.decodeUnknownEffect(Schema.Array(FileDiff.Info))
const state: State.Interface<Data, VcsDraft> = State.create<Data, VcsDraft>({
const state: State.Interface<Data, VcsEditor> = State.create<Data, VcsEditor>({
name: "vcs",
initial: () => ({ providers: new Map() }),
draft: (draft) => ({
add: (provider) => draft.providers.set(provider.id, provider),
editor: (editor) => ({
add: (provider) => editor.providers.set(provider.id, provider),
default: {
get: () => draft.selection,
set: (selection) => (draft.selection = selection),
get: () => editor.selection,
set: (selection) => (editor.selection = selection),
},
}),
notify: () => State.reconcile(root, fork, () => refresh()),
+7 -7
View File
@@ -51,7 +51,7 @@ export class RequestError extends Schema.TaggedError<RequestError>()("WebSearch.
export type Error = ProviderRequiredError | ProviderNotFoundError | DisabledError | RequestError
export interface Interface extends State.Transformable<Draft> {
export interface Interface extends State.Transformable<Editor> {
readonly providers: () => Effect.Effect<readonly Provider[]>
readonly default: () => Effect.Effect<Provider | undefined, DisabledError>
readonly select: (selection: Selection) => Effect.Effect<void>
@@ -65,7 +65,7 @@ type Data = {
selection?: Selection
}
export type Draft = {
export type Editor = {
add: (provider: ProviderImplementation) => void
default: {
get: () => Selection | undefined
@@ -79,13 +79,13 @@ const layer = Layer.effect(
const bus = yield* Bus.Service
const kv = yield* KV.Service
const decodeResults = Schema.decodeUnknownEffect(Schema.Array(Result))
const state = State.create<Data, Draft>({
const state = State.create<Data, Editor>({
initial: () => ({ providers: new Map() }),
draft: (draft) => ({
add: (provider) => draft.providers.set(provider.id, provider),
editor: (editor) => ({
add: (provider) => editor.providers.set(provider.id, provider),
default: {
get: () => draft.selection,
set: (selection) => (draft.selection = selection),
get: () => editor.selection,
set: (selection) => (editor.selection = selection),
},
}),
notify: () => bus.publish(WebSearch.Event.Updated, {}).pipe(Effect.asVoid),
+3 -3
View File
@@ -11,13 +11,13 @@ export const Plugin = define({
const bus = yield* Bus.Service
const wellknown = yield* WellKnown.Service
yield* wellknown.entries().pipe(Effect.orDie)
yield* ctx.integration.transform((draft) => {
yield* ctx.integration.transform((editor) => {
wellknown.snapshot().forEach((entry) => {
if (!entry.manifest.auth) return
draft.update(entry.integrationID, (integration) => {
editor.update(entry.integrationID, (integration) => {
integration.name = new URL(entry.origin).hostname
})
draft.method.update({
editor.method.update({
integrationID: entry.integrationID,
method: {
id: "login",
+5 -5
View File
@@ -38,8 +38,8 @@ describe("Catalog", () => {
const catalog = yield* Catalog.Service
const providerID = Provider.ID.make("resolve-fixture")
const modelID = Model.ID.make("fixture-model")
yield* catalog.transform((draft) =>
draft.model.update(providerID, modelID, (model) => {
yield* catalog.transform((editor) =>
editor.model.update(providerID, modelID, (model) => {
model.package = path === "aisdk" ? Provider.aisdk("@ai-sdk/fixture") : "@opencode-ai/ai/providers/openai"
model.settings = {
apiKey: path === "empty-key" ? "" : "fixture-key",
@@ -62,8 +62,8 @@ describe("Catalog", () => {
},
)
yield* catalog.transform((draft) =>
draft.model.update(providerID, modelID, (model) => {
yield* catalog.transform((editor) =>
editor.model.update(providerID, modelID, (model) => {
model.limit.context = 100_000
model.capabilities.tools = false
model.variants.push({ id: Model.VariantID.make("other") })
@@ -323,7 +323,7 @@ describe("Catalog", () => {
const providerID = Provider.ID.make("test")
const old = Model.ID.make("old")
const newest = Model.ID.make("new")
const models = (catalog: Catalog.Draft) => {
const models = (catalog: Catalog.Editor) => {
catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, old, (model) => {
model.time.released = 1000
+3 -3
View File
@@ -10,9 +10,9 @@ describe("CodeMode", () => {
it.effect("owns registrations, execute, and catalog materialization", () =>
Effect.gen(function* () {
const tools = yield* Tool.Service
yield* tools.transform((draft) => {
draft.namespace({ name: "empty", description: "No tools registered yet" })
draft.add({
yield* tools.transform((editor) => {
editor.namespace({ name: "empty", description: "No tools registered yet" })
editor.add({
name: "echo",
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
@@ -94,10 +94,10 @@ describe("CodeModeInstructions", () => {
const tools = yield* Tool.Service
const initialized = yield* Effect.scoped(
Effect.gen(function* () {
yield* tools.transform((draft) => {
draft.namespace({ name: "tools", description: "Project utilities" })
draft.add({ ...zeta, options: { namespace: "tools" } })
draft.add({ ...alpha, options: { namespace: "tools" } })
yield* tools.transform((editor) => {
editor.namespace({ name: "tools", description: "Project utilities" })
editor.add({ ...zeta, options: { namespace: "tools" } })
editor.add({ ...alpha, options: { namespace: "tools" } })
})
const snapshot = yield* tools.snapshot()
return yield* readInitial(CodeModeInstructions.make(snapshot.codeModeCatalog))
@@ -105,10 +105,10 @@ describe("CodeModeInstructions", () => {
)
const reordered = yield* Effect.scoped(
Effect.gen(function* () {
yield* tools.transform((draft) => {
draft.namespace({ name: "tools", description: "Project utilities" })
draft.add({ ...alpha, options: { namespace: "tools" } })
draft.add({ ...zeta, options: { namespace: "tools" } })
yield* tools.transform((editor) => {
editor.namespace({ name: "tools", description: "Project utilities" })
editor.add({ ...alpha, options: { namespace: "tools" } })
editor.add({ ...zeta, options: { namespace: "tools" } })
})
const snapshot = yield* tools.snapshot()
return yield* readUpdate(CodeModeInstructions.make(snapshot.codeModeCatalog), initialized)
+7 -7
View File
@@ -12,8 +12,8 @@ describe("Command", () => {
Effect.gen(function* () {
const command = yield* Command.Service
const calls: Command.Invocation[] = []
yield* command.transform((draft) => {
draft.add({
yield* command.transform((editor) => {
editor.add({
name: "goal",
description: "Manage the session goal",
execute: (input) => Effect.sync(() => calls.push(input)),
@@ -36,9 +36,9 @@ describe("Command", () => {
it.effect("replaces commands with later definitions", () =>
Effect.gen(function* () {
const command = yield* Command.Service
yield* command.transform((draft) => {
draft.add({ name: "goal", description: "First", execute: () => Effect.void })
draft.add({ name: "goal", description: "Second", execute: () => Effect.void })
yield* command.transform((editor) => {
editor.add({ name: "goal", description: "First", execute: () => Effect.void })
editor.add({ name: "goal", description: "Second", execute: () => Effect.void })
})
expect(yield* command.list()).toEqual([Command.Info.make({ name: "goal", description: "Second" })])
@@ -48,8 +48,8 @@ describe("Command", () => {
it.effect("returns callback error messages without stack traces", () =>
Effect.gen(function* () {
const command = yield* Command.Service
yield* command.transform((draft) => {
draft.add({
yield* command.transform((editor) => {
editor.add({
name: "fail",
execute: () => Effect.fail(new Error("command failed")),
})
+8 -8
View File
@@ -92,11 +92,11 @@ describe("ConfigProviderPlugin.Plugin", () => {
const providerID = Provider.ID.make("custom")
const inheritedID = Model.ID.make("inherited")
const overriddenID = Model.ID.make("overridden")
yield* catalog.transform((draft) => {
draft.model.update(providerID, inheritedID, (model) => {
yield* catalog.transform((editor) => {
editor.model.update(providerID, inheritedID, (model) => {
model.capabilities = { tools: false, input: ["text"], output: ["text"] }
})
draft.model.update(providerID, overriddenID, (model) => {
editor.model.update(providerID, overriddenID, (model) => {
model.capabilities = { tools: false, input: ["text"], output: ["text"] }
})
})
@@ -273,8 +273,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
const catalog = yield* Catalog.Service
const providerID = Provider.ID.make("custom")
const modelID = Model.ID.make("chat")
yield* catalog.transform((draft) => {
draft.model.update(providerID, modelID, (model) => {
yield* catalog.transform((editor) => {
editor.model.update(providerID, modelID, (model) => {
model.package = "aisdk:@ai-sdk/anthropic"
model.settings = { baseURL: "https://catalog.example/v1" }
model.capabilities = { tools: false, input: ["audio"], output: ["audio"] }
@@ -480,11 +480,11 @@ describe("ConfigProviderPlugin.Plugin", () => {
}),
]
yield* catalog.transform((draft) => {
draft.provider.update(Provider.ID.anthropic, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.anthropic, (provider) => {
provider.package = "aisdk:@ai-sdk/anthropic"
})
draft.model.update(Provider.ID.anthropic, modelID, (model) => {
editor.model.update(Provider.ID.anthropic, modelID, (model) => {
model.variants = [{ id: Model.VariantID.make("fast"), settings: { effort: "high" } }]
})
})
+2 -2
View File
@@ -39,8 +39,8 @@ describe("config plugin reloads", () => {
const plugins = yield* Plugin.Service
const references = yield* Reference.Service
const host = yield* PluginHost.make(plugins)
yield* references.transform((draft) =>
draft.add(
yield* references.transform((editor) =>
editor.add(
"external",
Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/references/external") }),
),
+2 -2
View File
@@ -20,8 +20,8 @@ describe("ConfigWebSearchPlugin.Plugin", () => {
const bus = yield* Bus.Service
const config = yield* Config.Test
const plugins = yield* Plugin.Service
yield* websearch.transform((draft) =>
draft.add({ id: WebSearch.ID.make("test"), name: "Test", execute: () => Effect.succeed([]) }),
yield* websearch.transform((editor) =>
editor.add({ id: WebSearch.ID.make("test"), name: "Test", execute: () => Effect.succeed([]) }),
)
yield* ConfigWebSearchPlugin.Plugin.effect(yield* PluginHost.make(plugins))
@@ -21,9 +21,9 @@ describe("LocationWatcherPolicy", () => {
yield* State.batch(
Effect.gen(function* () {
yield* policy.transform((draft) => draft.add(["node_modules"]))
yield* policy.transform((editor) => editor.add(["node_modules"]))
expect(policy.current()).toEqual(["node_modules"])
const overlay = yield* policy.transform((draft) => draft.add([".git"]))
const overlay = yield* policy.transform((editor) => editor.add([".git"]))
expect(policy.current()).toEqual(["node_modules", ".git"])
expect(observed).toEqual([])
@@ -47,7 +47,7 @@ describe("LocationWatcherPolicy", () => {
Effect.gen(function* () {
if (reentered) return
reentered = true
yield* policy.transform((draft) => draft.add([".git"])).pipe(Scope.provide(scope))
yield* policy.transform((editor) => editor.add([".git"])).pipe(Scope.provide(scope))
expect(policy.current()).toEqual(["node_modules", ".git"])
}),
)
@@ -58,7 +58,7 @@ describe("LocationWatcherPolicy", () => {
}),
)
yield* policy.transform((draft) => draft.add(["node_modules"]))
yield* policy.transform((editor) => editor.add(["node_modules"]))
expect(policy.current()).toEqual(["node_modules", ".git"])
expect(observed).toEqual([
@@ -306,7 +306,7 @@ describe("LocationWatcher subscriptions", () => {
layer: Layer.effectDiscard(
Effect.gen(function* () {
const policy = yield* LocationWatcherPolicy.Service
yield* policy.transform((draft) => draft.add([".git"]))
yield* policy.transform((editor) => editor.add([".git"]))
}),
),
deps: [LocationWatcherPolicy.node],
+4 -4
View File
@@ -198,9 +198,9 @@ describe("Formatter", () => {
withFormatter(false, (formatter, directory) =>
Effect.gen(function* () {
const command = { suffix: "A" }
yield* formatter.transform((draft) => {
yield* formatter.transform((editor) => {
const suffix = command.suffix
draft.set({
editor.set({
name: "reload",
extensions: [".reload"],
enabled: Effect.succeed([
@@ -230,7 +230,7 @@ describe("Formatter", () => {
const resolving = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const command = { suffix: "A" }
yield* formatter.transform((draft) => {
yield* formatter.transform((editor) => {
const suffix = command.suffix
const resolved = [
process.execPath,
@@ -238,7 +238,7 @@ describe("Formatter", () => {
`const fs = require('fs'); const file = process.argv.at(-1); fs.appendFileSync(file, '${suffix}')`,
"$FILE",
]
draft.set({
editor.set({
name: "reload-race",
extensions: [".race"],
enabled:
@@ -79,15 +79,15 @@ describe("InstructionDiscovery", () => {
it.effect("stores ordered values with last-write-wins precedence", () =>
Effect.gen(function* () {
const discovery = yield* InstructionDiscovery.Service
yield* discovery.transform((draft) => {
draft.add(file("/repo/AGENTS.md", "first"))
draft.add(file("/repo/packages/AGENTS.md", "package"))
draft.add(file("/repo/AGENTS.md", "last"))
draft.update("/repo/packages/AGENTS.md", (current) => {
yield* discovery.transform((editor) => {
editor.add(file("/repo/AGENTS.md", "first"))
editor.add(file("/repo/packages/AGENTS.md", "package"))
editor.add(file("/repo/AGENTS.md", "last"))
editor.update("/repo/packages/AGENTS.md", (current) => {
current.content = "updated"
current.path = AbsolutePath.make("/ignored")
})
draft.remove("/missing")
editor.remove("/missing")
})
expect(yield* discovery.list()).toEqual([
@@ -100,7 +100,7 @@ describe("InstructionDiscovery", () => {
it.effect("preserves admitted values while the source is unavailable", () =>
Effect.gen(function* () {
const discovery = yield* InstructionDiscovery.Service
yield* discovery.transform((draft) => draft.unavailable())
yield* discovery.transform((editor) => editor.unavailable())
expect(
(yield* readUpdate(
yield* discovery.load(),
@@ -113,16 +113,16 @@ describe("InstructionDiscovery", () => {
it.effect("renders granular instruction updates", () =>
Effect.gen(function* () {
const discovery = yield* InstructionDiscovery.Service
yield* discovery.transform((draft) => {
draft.add(file("/global/AGENTS.md", "global"))
draft.add(
yield* discovery.transform((editor) => {
editor.add(file("/global/AGENTS.md", "global"))
editor.add(
file("/repo/AGENTS.md", ["old", ...Array.from({ length: 20 }, (_, index) => `keep ${index}`)].join("\n")),
)
})
const initial = yield* readInitial(yield* discovery.load())
yield* discovery.transform((draft) => {
draft.update("/repo/AGENTS.md", (current) => {
yield* discovery.transform((editor) => {
editor.update("/repo/AGENTS.md", (current) => {
current.content = ["new", ...Array.from({ length: 20 }, (_, index) => `keep ${index}`)].join("\n")
})
})
@@ -134,9 +134,9 @@ describe("InstructionDiscovery", () => {
const rewritten = state({
"core/instructions": [{ path: "/repo/AGENTS.md", content: "old one\nold two\nold three\nold four" }],
})
yield* discovery.transform((draft) => {
draft.remove("/global/AGENTS.md")
draft.update("/repo/AGENTS.md", (current) => {
yield* discovery.transform((editor) => {
editor.remove("/global/AGENTS.md")
editor.update("/repo/AGENTS.md", (current) => {
current.content = "new"
})
})
@@ -144,8 +144,8 @@ describe("InstructionDiscovery", () => {
"The instructions changed:\nInstructions from: /repo/AGENTS.md\nnew",
)
yield* discovery.transform((draft) => {
draft.add(file("/repo/packages/AGENTS.md", "package"))
yield* discovery.transform((editor) => {
editor.add(file("/repo/packages/AGENTS.md", "package"))
})
const structural = (yield* readUpdate(yield* discovery.load(), initial)).text
expect(structural).toContain("The instructions from /global/AGENTS.md no longer apply.")
+30 -30
View File
@@ -1297,8 +1297,8 @@ testEffect(Layer.empty).live(
const original = JSON.stringify(entries)
yield* Effect.gen(function* () {
const service = yield* Mcp.Service
const check = yield* service.transform((draft) => {
expect(draft.get("resources")).toEqual({
const check = yield* service.transform((editor) => {
expect(editor.get("resources")).toEqual({
type: "local",
command: ["later"],
disabled: true,
@@ -1314,8 +1314,8 @@ testEffect(Layer.empty).live(
} satisfies ConfigMCP.Local
yield* service.add("resources", runtime)
yield* service.reload()
yield* service.transform((draft) => {
expect(draft.get("resources")).toEqual(runtime)
yield* service.transform((editor) => {
expect(editor.get("resources")).toEqual(runtime)
})
}).pipe(
Effect.provide(
@@ -1336,8 +1336,8 @@ testEffect(resourceMcpLayer(new ConfigMCP.Local({ type: "local", command: ["unus
yield* Effect.scoped(
Effect.gen(function* () {
yield* service.transform((draft) => {
draft.set("dynamic", {
yield* service.transform((editor) => {
editor.set("dynamic", {
type: "local",
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-output-schema.ts")],
})
@@ -1347,12 +1347,12 @@ testEffect(resourceMcpLayer(new ConfigMCP.Local({ type: "local", command: ["unus
)
expect(yield* service.tools()).toHaveLength(2)
yield* service.transform((draft) => draft.update("dynamic", (server) => (server.codemode = false)))
yield* service.transform((editor) => editor.update("dynamic", (server) => (server.codemode = false)))
expect((yield* service.tools()).map((tool) => tool.codemode)).toEqual([false, false])
const settings = { disabled: true }
yield* service.transform((draft) => {
draft.update("dynamic", (server) => {
yield* service.transform((editor) => {
editor.update("dynamic", (server) => {
server.disabled = settings.disabled
})
})
@@ -1366,7 +1366,7 @@ testEffect(resourceMcpLayer(new ConfigMCP.Local({ type: "local", command: ["unus
)
expect(yield* service.tools()).toHaveLength(2)
const removed = yield* service.transform((draft) => draft.remove("dynamic"))
const removed = yield* service.transform((editor) => editor.remove("dynamic"))
expect((yield* service.servers()).some((server) => server.name === "dynamic")).toBe(false)
expect(yield* service.tools()).toEqual([])
@@ -1396,14 +1396,14 @@ test("restores runtime MCP config when a transform is disposed", async () => {
disabled: true,
})
yield* service.add("dynamic", config)
const transformed = yield* service.transform((draft) =>
draft.update("dynamic", (server) => {
const transformed = yield* service.transform((editor) =>
editor.update("dynamic", (server) => {
if (server.type === "remote") server.headers = { Authorization: "transformed" }
}),
)
let observed: string | undefined
yield* service.transform((draft) => {
const server = draft.get("dynamic")
yield* service.transform((editor) => {
const server = editor.get("dynamic")
observed = server?.type === "remote" ? server.headers?.Authorization : undefined
})
@@ -1432,8 +1432,8 @@ test("isolates nested configured MCP mutations and reconciles them", async () =>
Effect.gen(function* () {
const service = yield* Mcp.Service
expect(published.filter((type) => type === McpEvent.StatusChanged.type)).toHaveLength(1)
yield* service.transform((draft) =>
draft.update("resources", (server) => {
yield* service.transform((editor) =>
editor.update("resources", (server) => {
if (server.type === "remote" && server.headers) server.headers.Authorization = "transformed"
}),
)
@@ -1452,16 +1452,16 @@ testEffect(Layer.empty).live("batches MCP transforms without connecting intermed
const service = yield* Mcp.Service
const registrations = yield* State.batch(
Effect.gen(function* () {
const added = yield* service.transform((draft) =>
draft.set("dynamic", {
const added = yield* service.transform((editor) =>
editor.set("dynamic", {
type: "remote",
url: server.url,
oauth: false,
}),
)
expect((yield* service.servers()).some((server) => server.name === "dynamic")).toBe(false)
const disabled = yield* service.transform((draft) =>
draft.update("dynamic", (config) => (config.disabled = true)),
const disabled = yield* service.transform((editor) =>
editor.update("dynamic", (config) => (config.disabled = true)),
)
return [added, disabled]
}),
@@ -1587,13 +1587,13 @@ testEffect(Layer.empty).live("keeps MCP config snapshots stable during an in-fli
yield* Effect.gen(function* () {
const service = yield* Mcp.Service
const replacing = yield* service
.transform((draft) => draft.update("resources", (config) => (config.disabled = false)))
.transform((editor) => editor.update("resources", (config) => (config.disabled = false)))
.pipe(Effect.forkScoped({ startImmediately: true }))
yield* Deferred.await(started)
const restoring = yield* State.batch(
Effect.gen(function* () {
yield* service.transform((draft) => draft.update("resources", (config) => (config.disabled = true)))
yield* service.transform((editor) => editor.update("resources", (config) => (config.disabled = true)))
yield* Deferred.succeed(accepted, undefined)
}),
).pipe(Effect.forkScoped({ startImmediately: true }))
@@ -1657,9 +1657,9 @@ shutdownIt.effect("discards in-flight and queued MCP notifications after its lay
)
const source = { url: "https://example.com/initial", added: false }
yield* service
.transform((draft) => {
draft.set("fixture", { type: "remote", url: source.url, oauth: false, disabled: true })
if (source.added) draft.set("queued", { type: "local", command: ["unused"], disabled: true })
.transform((editor) => {
editor.set("fixture", { type: "remote", url: source.url, oauth: false, disabled: true })
if (source.added) editor.set("queued", { type: "local", command: ["unused"], disabled: true })
})
.pipe(Scope.provide(root))
@@ -1758,8 +1758,8 @@ testEffect(Layer.empty).live("isolates invalid MCP tools and preserves plugin tr
"other_lookup",
"execute",
])
const override = yield* registry.transform((draft) => {
draft.add({
const override = yield* registry.transform((editor) => {
editor.add({
name: "search",
options: { namespace: "demo", codemode: false },
description: "Override search",
@@ -1768,11 +1768,11 @@ testEffect(Layer.empty).live("isolates invalid MCP tools and preserves plugin tr
execute: () => Effect.succeed({ output: "override" }),
})
})
const mutation = yield* registry.transform((draft) => {
draft.update("other_lookup", (tool) => {
const mutation = yield* registry.transform((editor) => {
editor.update("other_lookup", (tool) => {
tool.description += " updated"
})
draft.remove("repaired_lookup")
editor.remove("repaired_lookup")
})
yield* Ref.set(catalog, [tool("demo", "y".repeat(65)), ...healthy, tool("demo", "added"), namespace])
+6 -6
View File
@@ -116,7 +116,7 @@ it.effect("unloading a plugin removes its commands and runs cleanup", () =>
cleaned = true
}),
)
yield* ctx.command.transform((draft) => draft.add({ name: "greet", execute: () => Effect.void }))
yield* ctx.command.transform((editor) => editor.add({ name: "greet", execute: () => Effect.void }))
}),
},
])
@@ -141,7 +141,7 @@ it.effect("reports a failed plugin without blocking a healthy plugin", () =>
revision: "1",
effect: (ctx) =>
ctx.command
.transform((draft) => draft.add({ name: "greet", execute: () => Effect.void }))
.transform((editor) => editor.add({ name: "greet", execute: () => Effect.void }))
.pipe(Effect.asVoid),
},
])
@@ -166,8 +166,8 @@ it.effect("reloading a plugin replaces its command implementation", () =>
revision,
effect: (ctx) =>
ctx.command
.transform((draft) =>
draft.add({
.transform((editor) =>
editor.add({
name: "greet",
execute: () =>
Effect.sync(() => {
@@ -225,8 +225,8 @@ it.effect("refreshes expired OAuth credentials through the context during activa
revision: "1",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.integration.transform((draft) =>
draft.method.update({
yield* ctx.integration.transform((editor) =>
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: "Fixture" },
authorize: () => Effect.die("unexpected authorization"),
@@ -3,8 +3,8 @@ import { Plugin } from "@opencode-ai/plugin"
export default Plugin.define({
id: "greeting",
async setup(ctx) {
await ctx.command.transform((draft) =>
draft.add({
await ctx.command.transform((editor) =>
editor.add({
name: "greet",
description: ctx.options.description,
execute: async () => {},
+31 -31
View File
@@ -193,21 +193,21 @@ export function agentHost(agent: Agent.Interface): Plugin.Context["agent"] {
list: () => Effect.die("unused agent.list"),
reload: agent.reload,
transform: (callback) =>
agent.transform((draft) =>
agent.transform((editor) =>
callback({
list: () => draft.list().map(agentInfo),
list: () => editor.list().map(agentInfo),
get: (id) => {
const value = draft.get(Agent.ID.make(id))
const value = editor.get(Agent.ID.make(id))
return value && agentInfo(value)
},
default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)),
default: (id) => editor.default(id === undefined ? undefined : Agent.ID.make(id)),
update: (id, update) =>
draft.update(Agent.ID.make(id), (value) => {
editor.update(Agent.ID.make(id), (value) => {
const current = agentInfo(value)
update(current)
Object.assign(value, current, { id: Agent.ID.make(current.id) })
}),
remove: (id) => draft.remove(Agent.ID.make(id)),
remove: (id) => editor.remove(Agent.ID.make(id)),
}),
),
}
@@ -238,16 +238,16 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog
},
reload: catalog.reload,
transform: (callback) =>
catalog.transform((draft) =>
catalog.transform((editor) =>
callback({
provider: {
list: () =>
draft.provider.list().map((value) => ({
editor.provider.list().map((value) => ({
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
})),
get: (id) => {
const value = draft.provider.get(Provider.ID.make(id))
const value = editor.provider.get(Provider.ID.make(id))
return (
value && {
provider: providerInfo(value.provider),
@@ -256,20 +256,20 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog
)
},
update: (id, update) =>
draft.provider.update(Provider.ID.make(id), (value) => {
editor.provider.update(Provider.ID.make(id), (value) => {
const current = providerInfo(value)
update(current)
Object.assign(value, current, { id: Provider.ID.make(current.id) })
}),
remove: (id) => draft.provider.remove(Provider.ID.make(id)),
remove: (id) => editor.provider.remove(Provider.ID.make(id)),
},
model: {
get: (providerID, modelID) => {
const value = draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))
const value = editor.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))
return value && modelInfo(value)
},
update: (providerID, modelID, update) =>
draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), (value) => {
editor.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), (value) => {
const current = modelInfo(value)
update(current)
Object.assign(value, current, {
@@ -282,14 +282,14 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog
})),
})
}),
remove: (providerID, modelID) => draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)),
remove: (providerID, modelID) => editor.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)),
default: {
get: () => {
const value = draft.model.default.get()
const value = editor.model.default.get()
return value && { providerID: value.providerID, modelID: value.modelID }
},
set: (providerID, modelID) =>
draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)),
editor.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)),
},
},
}),
@@ -324,22 +324,22 @@ export function integrationHost(integration: Integration.Interface): Plugin.Cont
),
},
transform: (callback) =>
integration.transform((draft) =>
integration.transform((editor) =>
callback({
list: () => draft.list().map((value) => ({ id: value.id, name: value.name })),
list: () => editor.list().map((value) => ({ id: value.id, name: value.name })),
get: (id) => {
const value = draft.get(Integration.ID.make(id))
const value = editor.get(Integration.ID.make(id))
return value && { id: value.id, name: value.name }
},
update: (id, update) => draft.update(Integration.ID.make(id), update),
remove: (id) => draft.remove(Integration.ID.make(id)),
update: (id, update) => editor.update(Integration.ID.make(id), update),
remove: (id) => editor.remove(Integration.ID.make(id)),
method: {
list: (id) => draft.method.list(Integration.ID.make(id)),
list: (id) => editor.method.list(Integration.ID.make(id)),
update: (input) => {
if ("authorize" in input) {
const methodID = Integration.MethodID.make(input.method.id)
const refresh = input.refresh
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, id: methodID },
authorize: (answer) =>
@@ -390,14 +390,14 @@ export function integrationHost(integration: Integration.Interface): Plugin.Cont
return
}
if (input.method.type === "env") {
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
})
return
}
if (input.method.type === "command") {
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: {
...input.method,
@@ -406,12 +406,12 @@ export function integrationHost(integration: Integration.Interface): Plugin.Cont
})
return
}
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
})
},
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
remove: (id, item) => editor.method.remove(Integration.ID.make(id), internalMethod(item)),
},
}),
),
@@ -435,18 +435,18 @@ export function webSearchHost(websearch: WebSearch.Interface): Plugin.Context["w
.pipe(Effect.map((data) => ({ location, data }))),
reload: websearch.reload,
transform: (callback) =>
websearch.transform((draft) => {
websearch.transform((editor) => {
callback({
add: (definition) =>
draft.add({
editor.add({
id: WebSearch.ID.make(definition.id),
name: definition.name,
execute: definition.execute,
}),
default: {
get: draft.default.get,
get: editor.default.get,
set: (selection) =>
draft.default.set(
editor.default.set(
selection === false || selection === "random" ? selection : WebSearch.ID.make(selection),
),
},
+2 -2
View File
@@ -76,8 +76,8 @@ describe("ModelsDevPlugin", () => {
const scope = yield* Scope.make()
yield* catalog
.transform((draft) =>
draft.model.update(providerID, modelID, (model) => {
.transform((editor) =>
editor.model.update(providerID, modelID, (model) => {
model.variants ??= []
model.variants.push({ id: Model.VariantID.make("configured") })
}),
@@ -244,12 +244,12 @@ describe("AzurePlugin", () => {
() =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((draft) => {
draft.provider.update(Provider.ID.azure, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.azure, (provider) => {
provider.package = Provider.aisdk("@ai-sdk/azure")
})
draft.model.update(Provider.ID.azure, Model.ID.make("gpt-5-mini"), () => {})
draft.model.update(Provider.ID.azure, Model.ID.make("gpt-5-nano"), () => {})
editor.model.update(Provider.ID.azure, Model.ID.make("gpt-5-mini"), () => {})
editor.model.update(Provider.ID.azure, Model.ID.make("gpt-5-nano"), () => {})
})
yield* azureCredential
yield* addPlugin()
@@ -462,24 +462,24 @@ describe("AzurePlugin", () => {
gateway: Model.ID.make("gateway"),
nonAzure: Model.ID.make("non-azure"),
}
yield* catalog.transform((draft) => {
draft.provider.update(Provider.ID.azure, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.azure, (provider) => {
provider.package = Provider.aisdk("@ai-sdk/azure")
})
draft.model.update(Provider.ID.azure, models.responses, () => {})
draft.model.update(Provider.ID.azure, models.chat, (model) => {
editor.model.update(Provider.ID.azure, models.responses, () => {})
editor.model.update(Provider.ID.azure, models.chat, (model) => {
model.settings = { useCompletionUrls: true }
})
draft.model.update(Provider.ID.azure, models.preview, (model) => {
editor.model.update(Provider.ID.azure, models.preview, (model) => {
model.settings = { apiVersion: "2025-04-01-preview" }
})
draft.model.update(Provider.ID.azure, models.deploymentURL, (model) => {
editor.model.update(Provider.ID.azure, models.deploymentURL, (model) => {
model.settings = { useDeploymentBasedUrls: true }
})
draft.model.update(Provider.ID.azure, models.gateway, (model) => {
editor.model.update(Provider.ID.azure, models.gateway, (model) => {
model.settings = { baseURL: "https://gateway.example/azure" }
})
draft.model.update(Provider.ID.azure, models.nonAzure, (model) => {
editor.model.update(Provider.ID.azure, models.nonAzure, (model) => {
model.package = Provider.aisdk("@ai-sdk/anthropic")
})
})
@@ -56,12 +56,12 @@ describe("GithubCopilotPlugin", () => {
it.effect("removes the generic key method", () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* integrations.transform((draft) => {
draft.method.update({
yield* integrations.transform((editor) => {
editor.method.update({
integrationID: Integration.ID.make("github-copilot"),
method: { type: "key" },
})
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make("github-copilot"),
method: { type: "env", names: ["GITHUB_TOKEN"] },
})
@@ -289,22 +289,22 @@ describe("LMStudioPlugin", () => {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const providerID = Provider.ID.make("lmstudio")
yield* integrations.transform((draft) => {
draft.update(Integration.ID.make("lmstudio"), (integration) => {
yield* integrations.transform((editor) => {
editor.update(Integration.ID.make("lmstudio"), (integration) => {
integration.name = "LMStudio"
})
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make("lmstudio"),
method: { type: "env", names: ["LMSTUDIO_API_KEY"] },
})
})
yield* catalog.transform((draft) => {
draft.provider.update(providerID, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(providerID, (provider) => {
provider.name = "LMStudio"
provider.package = "aisdk:@ai-sdk/openai-compatible"
provider.integrationID = Integration.ID.make("lmstudio")
})
draft.model.update(providerID, Model.ID.make("static-model"), () => {})
editor.model.update(providerID, Model.ID.make("static-model"), () => {})
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).not.toContain(providerID)
@@ -319,11 +319,11 @@ describe("LMStudioPlugin", () => {
expect(yield* catalog.model.get(providerID, Model.ID.make("static-model"))).toBeUndefined()
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
yield* integrations.transform((draft) => {
draft.update(Integration.ID.make("lmstudio"), (integration) => {
yield* integrations.transform((editor) => {
editor.update(Integration.ID.make("lmstudio"), (integration) => {
integration.name = "Configured LM Studio"
})
draft.method.update({ integrationID: Integration.ID.make("lmstudio"), method: { type: "key" } })
editor.method.update({ integrationID: Integration.ID.make("lmstudio"), method: { type: "key" } })
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
@@ -188,22 +188,22 @@ describe("OllamaPlugin", () => {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const providerID = Provider.ID.make("ollama")
yield* integrations.transform((draft) => {
draft.update(Integration.ID.make("ollama"), (integration) => {
yield* integrations.transform((editor) => {
editor.update(Integration.ID.make("ollama"), (integration) => {
integration.name = "Ollama"
})
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make("ollama"),
method: { type: "env", names: ["OLLAMA_API_KEY"] },
})
})
yield* catalog.transform((draft) => {
draft.provider.update(providerID, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(providerID, (provider) => {
provider.name = "Ollama"
provider.package = "aisdk:@ai-sdk/openai-compatible"
provider.integrationID = Integration.ID.make("ollama")
})
draft.model.update(providerID, Model.ID.make("static-model"), () => {})
editor.model.update(providerID, Model.ID.make("static-model"), () => {})
})
yield* addPlugin(server.url.origin, "5 millis")
@@ -267,12 +267,12 @@ describe("OpencodePlugin", () => {
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
yield* catalog.transform((draft) => {
draft.provider.update(Provider.ID.openai, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.openai, (provider) => {
provider.package = Provider.aisdk("@ai-sdk/openai")
provider.integrationID = Integration.ID.make("openai")
})
draft.model.update(Provider.ID.openai, Model.ID.make("api-model"), (model) => {
editor.model.update(Provider.ID.openai, Model.ID.make("api-model"), (model) => {
model.package = Provider.aisdk("@ai-sdk/openai")
model.settings = { baseURL: "https://upstream.example/v1" }
model.variants = [
@@ -284,7 +284,7 @@ describe("OpencodePlugin", () => {
},
]
})
draft.model.update(Provider.ID.make("remote"), Model.ID.make("stale"), () => {})
editor.model.update(Provider.ID.make("remote"), Model.ID.make("stale"), () => {})
})
const initial = yield* credentials.create({
integrationID: Integration.ID.make("opencode"),
@@ -172,23 +172,23 @@ describe("VLLMPlugin", () => {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const providerID = Provider.ID.make("vllm")
yield* integrations.transform((draft) => {
draft.update(Integration.ID.make("vllm"), (integration) => {
yield* integrations.transform((editor) => {
editor.update(Integration.ID.make("vllm"), (integration) => {
integration.name = "vLLM"
})
draft.method.update({
editor.method.update({
integrationID: Integration.ID.make("vllm"),
method: { type: "env", names: ["VLLM_API_KEY"] },
})
})
yield* catalog.transform((draft) => {
draft.provider.update(providerID, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(providerID, (provider) => {
provider.name = "vLLM"
provider.package = "aisdk:@ai-sdk/openai-compatible"
provider.integrationID = Integration.ID.make("vllm")
provider.activation = "auto"
})
draft.model.update(providerID, Model.ID.make("static-model"), () => {})
editor.model.update(providerID, Model.ID.make("static-model"), () => {})
})
yield* addPlugin(server.url.origin, "5 millis")
@@ -73,11 +73,11 @@ describe("XAIPlugin", () => {
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = Provider.ID.make("xai")
yield* catalog.transform((draft) => {
draft.provider.update(providerID, (provider) => {
yield* catalog.transform((editor) => {
editor.provider.update(providerID, (provider) => {
provider.package = Provider.aisdk("@ai-sdk/xai")
})
draft.model.update(providerID, Model.ID.make("grok-4.6"), () => {})
editor.model.update(providerID, Model.ID.make("grok-4.6"), () => {})
})
yield* addPlugin()
+1 -1
View File
@@ -24,7 +24,7 @@ const greeter = (command: string) =>
define({
id,
effect: (ctx) =>
ctx.command.transform((draft) => draft.add({ name: command, execute: () => Effect.void })).pipe(Effect.asVoid),
ctx.command.transform((editor) => editor.add({ name: command, execute: () => Effect.void })).pipe(Effect.asVoid),
})
const instances = Layer.effect(
+12 -12
View File
@@ -21,7 +21,7 @@ const fallback = SessionSystemPrompt.make([])
const makeHost = Effect.gen(function* () {
const agents = yield* Agent.Service
const plugins = yield* Plugin.Service
yield* agents.transform((draft) => draft.update(Agent.ID.make("build"), () => {}))
yield* agents.transform((editor) => editor.update(Agent.ID.make("build"), () => {}))
return yield* PluginHost.make(plugins)
})
@@ -66,9 +66,9 @@ describe("SystemPromptPlugin", () => {
const catalog = yield* Catalog.Service
const hooks = yield* PluginHooks.Service
const pluginHost = yield* makeHost
yield* catalog.transform((draft) => {
yield* catalog.transform((editor) => {
for (const id of ["gpt-5", "gpt-4.1", "gpt-5-codex"])
draft.model.update(Provider.ID.make("test"), Model.ID.make(id), () => {})
editor.model.update(Provider.ID.make("test"), Model.ID.make(id), () => {})
})
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
discard: true,
@@ -108,8 +108,8 @@ describe("SystemPromptPlugin", () => {
const catalog = yield* Catalog.Service
const hooks = yield* PluginHooks.Service
const pluginHost = yield* makeHost
yield* catalog.transform((draft) =>
draft.model.update(Provider.ID.make("test"), Model.ID.make("gpt-5"), () => {}),
yield* catalog.transform((editor) =>
editor.model.update(Provider.ID.make("test"), Model.ID.make("gpt-5"), () => {}),
)
yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost)
const event = context("gpt-5")
@@ -154,8 +154,8 @@ describe("SystemPromptPlugin", () => {
Effect.gen(function* () {
const agents = yield* Agent.Service
const hooks = yield* PluginHooks.Service
yield* agents.transform((draft) =>
draft.update(Agent.ID.make("build"), (agent) => {
yield* agents.transform((editor) =>
editor.update(Agent.ID.make("build"), (agent) => {
agent.system = "Custom agent prompt"
}),
)
@@ -177,7 +177,7 @@ describe("SystemPromptPlugin", () => {
const hooks = yield* PluginHooks.Service
const pluginHost = yield* makeHost
yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost)
yield* agents.transform((draft) => draft.remove(Agent.ID.make("build")))
yield* agents.transform((editor) => editor.remove(Agent.ID.make("build")))
const event = context("gpt-5")
yield* hooks.trigger("session", "context", event)
@@ -207,14 +207,14 @@ describe("SystemPromptPlugin", () => {
const catalog = yield* Catalog.Service
const hooks = yield* PluginHooks.Service
const pluginHost = yield* makeHost
yield* catalog.transform((draft) => {
draft.model.update(Provider.ID.make("test"), Model.ID.make("openai-alias"), (model) => {
yield* catalog.transform((editor) => {
editor.model.update(Provider.ID.make("test"), Model.ID.make("openai-alias"), (model) => {
model.modelID = Model.ID.make("gpt-5")
})
draft.model.update(Provider.ID.make("test"), Model.ID.make("gpt-5-alias"), (model) => {
editor.model.update(Provider.ID.make("test"), Model.ID.make("gpt-5-alias"), (model) => {
model.modelID = Model.ID.make("custom-model")
})
draft.model.update(Provider.ID.make("test"), Model.ID.make("codex-family-alias"), (model) => {
editor.model.update(Provider.ID.make("test"), Model.ID.make("codex-family-alias"), (model) => {
model.modelID = Model.ID.make("custom-deployment")
model.family = Model.Family.make("gpt-codex")
})
+2 -2
View File
@@ -34,8 +34,8 @@ describe("built-in web search providers", () => {
Effect.gen(function* () {
const integrations = yield* Integration.Service
const websearch = yield* WebSearch.Service
const registration = yield* webSearchHost(websearch).transform((draft) => {
draft.add({
const registration = yield* webSearchHost(websearch).transform((editor) => {
editor.add({
id: "test-websearch",
name: "Test Web Search",
execute: (input) => Effect.succeed([{ url: "https://example.com", content: input.query, time: {} }]),
+1 -1
View File
@@ -212,7 +212,7 @@ describe("pty create defaults", () => {
if (!configuredShell) return
const pty = yield* Pty.Service
const shell = yield* ShellSelect.Service
yield* shell.transform((draft) => draft.configure(configuredShell))
yield* shell.transform((editor) => editor.configure(configuredShell))
const info = yield* Effect.acquireRelease(pty.create({ title: "configured" }), (created) =>
pty.remove(created.id).pipe(Effect.ignore),
)
+11 -11
View File
@@ -61,13 +61,13 @@ describe("Reference", () => {
yield* State.batch(
Effect.gen(function* () {
yield* references.transform((draft) =>
draft.add("docs", Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") })),
yield* references.transform((editor) =>
editor.add("docs", Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") })),
)
expect((yield* references.list()).map((info) => info.name)).toEqual(["docs"])
yield* references.transform((draft) => {
draft.add(
yield* references.transform((editor) => {
editor.add(
"sdk",
Reference.GitSource.make({
type: "git",
@@ -77,12 +77,12 @@ describe("Reference", () => {
hidden: true,
}),
)
draft.add("invalid", Reference.GitSource.make({ type: "git", repository: "invalid" }))
draft.add(
editor.add("invalid", Reference.GitSource.make({ type: "git", repository: "invalid" }))
editor.add(
"invalid-branch",
Reference.GitSource.make({ type: "git", repository: "owner/repo", branch: "../escape" }),
)
draft.add("file", Reference.GitSource.make({ type: "git", repository: "file:///docs" }))
editor.add("file", Reference.GitSource.make({ type: "git", repository: "file:///docs" }))
})
const infos = yield* references.list()
expect(infos.map((info) => info.name)).toEqual(["docs", "sdk"])
@@ -122,8 +122,8 @@ describe("Reference", () => {
if (event.type !== Reference.Event.Updated.type || reentered) return
reentered = true
yield* references
.transform((draft) =>
draft.add("docs", Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/new") })),
.transform((editor) =>
editor.add("docs", Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/new") })),
)
.pipe(Scope.provide(scope))
}),
@@ -139,8 +139,8 @@ describe("Reference", () => {
)
yield* Effect.addFinalizer(() => first.pipe(Effect.andThen(second)))
yield* references.transform((draft) =>
draft.add("docs", Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/old") })),
yield* references.transform((editor) =>
editor.add("docs", Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/old") })),
)
expect((yield* references.list()).map((info) => info.path)).toEqual([AbsolutePath.make("/new")])
+2 -2
View File
@@ -200,8 +200,8 @@ const setup = Effect.gen(function* () {
const instructionBuiltIns = yield* InstructionBuiltIns.Service
const context = yield* SessionContext.Service
const store = yield* SessionStore.Service
yield* agents.transform((draft) =>
draft.update(Agent.ID.make("build"), (agent) => {
yield* agents.transform((editor) =>
editor.update(Agent.ID.make("build"), (agent) => {
agent.mode = "primary"
}),
)
@@ -182,8 +182,8 @@ describe("SessionRunnerLLM recorded", () => {
const agents = yield* Agent.Service
const catalog = yield* Catalog.Service
const hooks = yield* PluginHooks.Service
yield* agents.transform((draft) =>
draft.update(Agent.ID.make("build"), (agent) => {
yield* agents.transform((editor) =>
editor.update(Agent.ID.make("build"), (agent) => {
agent.mode = "primary"
agent.permissions.push({ action: "execute", resource: "*", effect: "deny" })
}),
+11 -11
View File
@@ -257,8 +257,8 @@ const permissionFail = {
}
const permission = permissionLayer()
const transformTools = (registry: Tool.Interface, tools: Readonly<Record<string, ToolInfo>>, options?: Tool.Options) =>
registry.transform((draft) =>
Object.entries(tools).forEach(([name, tool]) => draft.add({ ...tool, name, options: options ?? tool.options })),
registry.transform((editor) =>
Object.entries(tools).forEach(([name, tool]) => editor.add({ ...tool, name, options: options ?? tool.options })),
)
const layer = Layer.unwrap(
Effect.map(RunnerState, (state) => {
@@ -521,8 +521,8 @@ const setup = Effect.gen(function* () {
yield* Effect.forEach(SystemPromptPlugin.Plugins, (plugin) => plugin.effect(pluginHost), {
discard: true,
})
yield* agents.transform((draft) =>
draft.update(Agent.ID.make("build"), (agent) => {
yield* agents.transform((editor) =>
editor.update(Agent.ID.make("build"), (agent) => {
agent.mode = "primary"
}),
)
@@ -906,8 +906,8 @@ const prepareTitleGeneration = Effect.gen(function* () {
const agents = yield* Agent.Service
const { db } = yield* Database.Service
yield* db.update(SessionTable).set({ title: null }).where(eq(SessionTable.id, sessionID)).run().pipe(Effect.orDie)
yield* agents.transform((draft) =>
draft.update(Agent.ID.make("title"), (agent) => {
yield* agents.transform((editor) =>
editor.update(Agent.ID.make("title"), (agent) => {
agent.mode = "primary"
agent.hidden = true
agent.system = "Generate a title."
@@ -1700,8 +1700,8 @@ describe("SessionRunnerLLM", () => {
scenario("updates selected-agent skill instructions after an agent switch", function* (s) {
const agents = yield* Agent.Service
yield* agents.transform((draft) =>
draft.update(Agent.ID.make("reviewer"), (agent) => {
yield* agents.transform((editor) =>
editor.update(Agent.ID.make("reviewer"), (agent) => {
agent.mode = "primary"
}),
)
@@ -2113,8 +2113,8 @@ describe("SessionRunnerLLM", () => {
provider: route === AnthropicMessages.route ? "anthropic" : "openai",
route,
})
yield* agents.transform((draft) =>
draft.update(agentID, (agent) => {
yield* agents.transform((editor) =>
editor.update(agentID, (agent) => {
agent.system = "Review the project carefully."
}),
)
@@ -2484,7 +2484,7 @@ describe("SessionRunnerLLM", () => {
scenario("does not recover provider context overflow when automatic compaction is disabled", function* (s) {
yield* setupOverflowRecovery(s)
const compaction = yield* SessionCompaction.Service
yield* compaction.transform((draft) => draft.configure({ auto: false }))
yield* compaction.transform((editor) => editor.configure({ auto: false }))
yield* s.llm.push(
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
TestLLM.text("Must not compact", "text-unexpected-summary"),
+17 -17
View File
@@ -23,11 +23,11 @@ describe("Skill", () => {
it.effect("registers values with last-write-wins precedence", () =>
Effect.gen(function* () {
const skill = yield* Skill.Service
yield* skill.transform((draft) => {
draft.add(info("review", "First"))
draft.add(info("deploy", "Deploy"))
draft.add(info("review", "Second"))
expect(draft.list().map((item) => item.id)).toEqual([Skill.ID.make("review"), Skill.ID.make("deploy")])
yield* skill.transform((editor) => {
editor.add(info("review", "First"))
editor.add(info("deploy", "Deploy"))
editor.add(info("review", "Second"))
expect(editor.list().map((item) => item.id)).toEqual([Skill.ID.make("review"), Skill.ID.make("deploy")])
})
expect(yield* skill.list()).toEqual([info("review", "Second"), info("deploy", "Deploy")])
@@ -39,17 +39,17 @@ describe("Skill", () => {
it.effect("updates and removes registered values", () =>
Effect.gen(function* () {
const skill = yield* Skill.Service
yield* skill.transform((draft) => {
draft.add(info("review", "Initial"))
draft.update("review", (value) => {
yield* skill.transform((editor) => {
editor.add(info("review", "Initial"))
editor.update("review", (value) => {
value.description = "Updated"
value.id = Skill.ID.make("ignored")
})
draft.update("missing", () => {
editor.update("missing", () => {
throw new Error("unreachable")
})
draft.add(info("deploy", "Deploy"))
draft.remove("deploy")
editor.add(info("deploy", "Deploy"))
editor.remove("deploy")
})
expect(yield* skill.list()).toEqual([info("review", "Updated")])
@@ -60,9 +60,9 @@ describe("Skill", () => {
Effect.gen(function* () {
const skill = yield* Skill.Service
const original = info("review", "Initial")
yield* skill.transform((draft) => draft.add(original))
const updated = yield* skill.transform((draft) =>
draft.update("review", (value) => {
yield* skill.transform((editor) => editor.add(original))
const updated = yield* skill.transform((editor) =>
editor.update("review", (value) => {
value.description = "Updated"
}),
)
@@ -85,7 +85,7 @@ describe("Skill", () => {
)
yield* Effect.yieldNow
yield* skill.transform((draft) => draft.add(info("review", "Visible")))
yield* skill.transform((editor) => editor.add(info("review", "Visible")))
expect(yield* Deferred.await(updated).pipe(Effect.timeout("1 second"))).toEqual([info("review", "Visible")])
yield* Fiber.interrupt(fiber)
}),
@@ -94,8 +94,8 @@ describe("Skill", () => {
it.effect("filters values by agent permissions", () =>
Effect.gen(function* () {
const agents = yield* Agent.Service
yield* agents.transform((draft) =>
draft.update(Agent.ID.make("reviewer"), (agent) => {
yield* agents.transform((editor) =>
editor.update(Agent.ID.make("reviewer"), (agent) => {
agent.permissions.push({ action: "skill", resource: "deploy", effect: "deny" })
}),
)
+1 -1
View File
@@ -166,7 +166,7 @@ describe("Snapshot", () => {
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const registration = yield* snapshot.transform((draft) => draft.configure(false))
const registration = yield* snapshot.transform((editor) => editor.configure(false))
expect(yield* snapshot.capture()).toBeUndefined()
yield* registration.dispose
+8 -8
View File
@@ -47,14 +47,14 @@ describe("State replay properties", () => {
const states = sources.map((_, index) =>
State.create({
initial: (): Value => ({ value: sources[index], order: [] }),
draft: (draft) => draft,
editor: (editor) => editor,
notify: () => Effect.sync(() => void notifications[index]++),
}),
)
const callbacks = operations.map((operation, index) => (draft: Value) => {
const callbacks = operations.map((operation, index) => (editor: Value) => {
calls++
draft.value = apply(draft.value, operation)
draft.order.push(index)
editor.value = apply(editor.value, operation)
editor.order.push(index)
})
const registrations: { handle: State.Registration; callback: number; active: boolean }[][] = [[], []]
const expected = (index: number): Value => {
@@ -138,7 +138,7 @@ describe("State replay properties", () => {
let calls = 0
const state = State.create({
initial: () => ({ value: 1, order: new Array<number>() }),
draft: (draft) => draft,
editor: (editor) => editor,
})
const registrations: State.Registration[] = []
const retained: { value: Value; snapshot: Value }[] = []
@@ -155,10 +155,10 @@ describe("State replay properties", () => {
Effect.gen(function* () {
const before = calls
const added = yield* Effect.forEach(chunk.append, (operation) =>
state.transform((draft) => {
state.transform((editor) => {
calls++
draft.value = apply(draft.value, operation)
draft.order.push(draft.order.length)
editor.value = apply(editor.value, operation)
editor.order.push(editor.order.length)
}),
)
registrations.push(...added)

Some files were not shown because too many files have changed in this diff Show More