|
|
@@ -0,0 +1,193 @@
|
|
|
+import { Agent } from "@/agent/agent"
|
|
|
+import { Command } from "@/command"
|
|
|
+import { InstanceRef } from "@/effect/instance-ref"
|
|
|
+import { InstanceStore } from "@/project/instance-store"
|
|
|
+import { ModelID, ProviderID } from "@/provider/schema"
|
|
|
+import { Provider } from "@/provider/provider"
|
|
|
+import { Context, Effect, Layer, SynchronizedRef } from "effect"
|
|
|
+
|
|
|
+export type ModelOption = {
|
|
|
+ readonly providerID: ProviderID
|
|
|
+ readonly providerName: string
|
|
|
+ readonly modelID: ModelID
|
|
|
+ readonly modelName: string
|
|
|
+}
|
|
|
+
|
|
|
+export type ModeOption = {
|
|
|
+ readonly id: string
|
|
|
+ readonly name: string
|
|
|
+ readonly description?: string
|
|
|
+}
|
|
|
+
|
|
|
+export type ModelVariants = NonNullable<Provider.Model["variants"]>
|
|
|
+
|
|
|
+export type DefaultModel = {
|
|
|
+ readonly providerID: ProviderID
|
|
|
+ readonly modelID: ModelID
|
|
|
+}
|
|
|
+
|
|
|
+export type Snapshot = {
|
|
|
+ readonly directory: string
|
|
|
+ readonly providers: Record<ProviderID, Provider.Info>
|
|
|
+ readonly modelOptions: readonly ModelOption[]
|
|
|
+ readonly variantsByModel: Readonly<Record<string, ModelVariants>>
|
|
|
+ readonly availableModes: readonly ModeOption[]
|
|
|
+ readonly defaultModeID: string
|
|
|
+ readonly availableCommands: readonly Command.Info[]
|
|
|
+ readonly defaultModel?: DefaultModel
|
|
|
+}
|
|
|
+
|
|
|
+export interface LoaderInterface {
|
|
|
+ readonly load: (directory: string) => Effect.Effect<Snapshot>
|
|
|
+}
|
|
|
+
|
|
|
+export interface Interface {
|
|
|
+ readonly get: (directory: string) => Effect.Effect<Snapshot>
|
|
|
+ readonly refresh: (directory: string) => Effect.Effect<Snapshot>
|
|
|
+ readonly variants: (snapshot: Snapshot, model: DefaultModel) => ModelVariants | undefined
|
|
|
+}
|
|
|
+
|
|
|
+export class Loader extends Context.Service<Loader, LoaderInterface>()("@opencode/ACPNextDirectoryLoader") {}
|
|
|
+
|
|
|
+export class Service extends Context.Service<Service, Interface>()("@opencode/ACPNextDirectory") {}
|
|
|
+
|
|
|
+export const modelKey = (model: DefaultModel) => `${model.providerID}/${model.modelID}`
|
|
|
+
|
|
|
+export const variants = (snapshot: Snapshot, model: DefaultModel) => snapshot.variantsByModel[modelKey(model)]
|
|
|
+
|
|
|
+export const build = (input: {
|
|
|
+ readonly directory: string
|
|
|
+ readonly providers: Record<ProviderID, Provider.Info>
|
|
|
+ readonly modes: readonly ModeOption[]
|
|
|
+ readonly defaultModeID: string
|
|
|
+ readonly commands: readonly Command.Info[]
|
|
|
+ readonly defaultModel?: DefaultModel
|
|
|
+}): Snapshot => {
|
|
|
+ const modelOptions = Provider.sort(
|
|
|
+ Object.values(input.providers).flatMap((provider) =>
|
|
|
+ Object.values(provider.models).map((model) => ({
|
|
|
+ id: model.id,
|
|
|
+ providerID: provider.id,
|
|
|
+ providerName: provider.name,
|
|
|
+ modelID: model.id,
|
|
|
+ modelName: model.name,
|
|
|
+ })),
|
|
|
+ ),
|
|
|
+ ).map((model) => ({
|
|
|
+ providerID: model.providerID,
|
|
|
+ providerName: model.providerName,
|
|
|
+ modelID: model.modelID,
|
|
|
+ modelName: model.modelName,
|
|
|
+ }))
|
|
|
+
|
|
|
+ return {
|
|
|
+ directory: input.directory,
|
|
|
+ providers: input.providers,
|
|
|
+ modelOptions,
|
|
|
+ variantsByModel: Object.fromEntries(
|
|
|
+ Object.values(input.providers).flatMap((provider) =>
|
|
|
+ Object.values(provider.models).flatMap((model) =>
|
|
|
+ model.variants ? [[modelKey({ providerID: provider.id, modelID: model.id }), model.variants]] : [],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ availableModes: input.modes,
|
|
|
+ defaultModeID: input.modes.some((mode) => mode.id === input.defaultModeID)
|
|
|
+ ? input.defaultModeID
|
|
|
+ : (input.modes[0]?.id ?? input.defaultModeID),
|
|
|
+ availableCommands: input.commands,
|
|
|
+ ...(input.defaultModel ? { defaultModel: input.defaultModel } : {}),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const loaderLayer = Layer.effect(
|
|
|
+ Loader,
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const store = yield* InstanceStore.Service
|
|
|
+ const provider = yield* Provider.Service
|
|
|
+ const agent = yield* Agent.Service
|
|
|
+ const command = yield* Command.Service
|
|
|
+
|
|
|
+ return Loader.of({
|
|
|
+ load: Effect.fn("ACPNextDirectoryLoader.load")(function* (directory) {
|
|
|
+ const ctx = yield* store.load({ directory })
|
|
|
+ return yield* Effect.gen(function* () {
|
|
|
+ const providers = yield* provider.list()
|
|
|
+ const [agents, defaultAgent, commands, defaultModel] = yield* Effect.all(
|
|
|
+ [
|
|
|
+ agent.list(),
|
|
|
+ agent.defaultInfo(),
|
|
|
+ command.list(),
|
|
|
+ provider.defaultModel().pipe(Effect.option),
|
|
|
+ ],
|
|
|
+ { concurrency: "unbounded" },
|
|
|
+ )
|
|
|
+ return build({
|
|
|
+ directory,
|
|
|
+ providers,
|
|
|
+ modes: agents
|
|
|
+ .filter((item) => item.mode !== "subagent" && item.hidden !== true)
|
|
|
+ .map((item) => ({
|
|
|
+ id: item.name,
|
|
|
+ name: item.name,
|
|
|
+ ...(item.description ? { description: item.description } : {}),
|
|
|
+ })),
|
|
|
+ defaultModeID: defaultAgent.name,
|
|
|
+ commands: commands.toSorted((a, b) => a.name.localeCompare(b.name)),
|
|
|
+ ...(defaultModel._tag === "Some" ? { defaultModel: defaultModel.value } : {}),
|
|
|
+ })
|
|
|
+ }).pipe(Effect.provideService(InstanceRef, ctx))
|
|
|
+ }),
|
|
|
+ })
|
|
|
+ }),
|
|
|
+)
|
|
|
+
|
|
|
+export const layer = Layer.effect(
|
|
|
+ Service,
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const loader = yield* Loader
|
|
|
+ const snapshots = yield* SynchronizedRef.make(new Map<string, Effect.Effect<Snapshot>>())
|
|
|
+
|
|
|
+ const cached = Effect.fnUntraced(function* (directory: string) {
|
|
|
+ return yield* SynchronizedRef.modifyEffect(
|
|
|
+ snapshots,
|
|
|
+ Effect.fnUntraced(function* (items) {
|
|
|
+ const current = items.get(directory)
|
|
|
+ if (current) return [current, items] as const
|
|
|
+ const next = yield* Effect.cached(loader.load(directory))
|
|
|
+ return [next, new Map(items).set(directory, next)] as const
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ const get = Effect.fn("ACPNextDirectory.get")(function* (directory: string) {
|
|
|
+ return yield* (yield* cached(directory))
|
|
|
+ })
|
|
|
+
|
|
|
+ const refresh = Effect.fn("ACPNextDirectory.refresh")(function* (directory: string) {
|
|
|
+ return yield* SynchronizedRef.modifyEffect(
|
|
|
+ snapshots,
|
|
|
+ Effect.fnUntraced(function* (items) {
|
|
|
+ const next = yield* Effect.cached(loader.load(directory))
|
|
|
+ return [next, new Map(items).set(directory, next)] as const
|
|
|
+ }),
|
|
|
+ ).pipe(Effect.flatten)
|
|
|
+ })
|
|
|
+
|
|
|
+ return Service.of({
|
|
|
+ get,
|
|
|
+ refresh,
|
|
|
+ variants,
|
|
|
+ })
|
|
|
+ }),
|
|
|
+)
|
|
|
+
|
|
|
+export const defaultLayer = layer.pipe(
|
|
|
+ Layer.provide(loaderLayer),
|
|
|
+ Layer.provide(Provider.defaultLayer),
|
|
|
+ Layer.provide(Agent.defaultLayer),
|
|
|
+ Layer.provide(Command.defaultLayer),
|
|
|
+ Layer.provide(InstanceStore.defaultLayer),
|
|
|
+)
|
|
|
+
|
|
|
+export * as Directory from "./directory"
|