feat(tui): refresh agents after update events

This commit is contained in:
Dax Raad 2026-06-29 18:52:00 -04:00
commit 02cb350880
17 changed files with 576 additions and 30 deletions

View file

@ -3,6 +3,7 @@ export * as AgentV2 from "./agent"
import { makeLocationNode } from "./effect/app-node"
import { Array, Context, Effect, Layer, Types } from "effect"
import { Agent } from "@opencode-ai/schema/agent"
import { EventV2 } from "./event"
import { State } from "./state"
export const ID = Agent.ID
@ -14,6 +15,8 @@ export const Color = Agent.Color
export const Info = Agent.Info
export type Info = Agent.Info
export const Event = Agent.Event
export interface Selection {
readonly id: ID
readonly info: Info | undefined
@ -45,6 +48,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const state = State.create<Data, Draft>({
initial: () => ({ agents: new Map() }),
draft: (draft) => ({
@ -63,6 +67,7 @@ export const layer = Layer.effect(
draft.agents.delete(id)
},
}),
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
})
const selectable = (agent: Info | undefined) =>
agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined
@ -108,4 +113,4 @@ export const layer = Layer.effect(
export const locationLayer = layer
export const node = makeLocationNode({ service: Service, layer, deps: [] })
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })

View file

@ -60,6 +60,7 @@ export const Plugin = define({
const configuredDefault = Config.latest(documents, "default_agent")
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
for (const current of draft.list()) {
yield* Effect.log({ msg: "applying permissions", id: current.id, permissions: global })
draft.update(current.id, (agent) => agent.permissions.push(...global))
}

View file

@ -76,7 +76,7 @@ export const Plugin = define({
? pathToFileURL(ref.package).href
: (yield* npm.add(ref.package)).entrypoint
if (!entrypoint) return
yield* Effect.log({ msg: "loading plugin", id: ref.package, entrypoint })
const mod = yield* Effect.promise(() => import(entrypoint))
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
@ -86,6 +86,6 @@ export const Plugin = define({
})
}).pipe(Effect.ignoreCause)
}
}).pipe(Effect.forkScoped({ startImmediately: true }))
})
}),
})

View file

@ -114,11 +114,11 @@ const layer = Layer.effectDiscard(
yield* add(CommandPlugin.Plugin)
yield* add(SkillPlugin.Plugin)
yield* add(ModelsDevPlugin)
yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin)
for (const item of ProviderPlugins) yield* add(item)
yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(VariantPlugin.Plugin)
// Embedder-contributed plugins are added last so they layer over config.

View file

@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { Effect, Exit, Scope } from "effect"
import { Effect, Exit, Fiber, Layer, Scope, Stream } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
import { AbsolutePath } from "@opencode-ai/core/schema"
@ -8,9 +9,32 @@ import { location } from "./fixture/location"
import { testEffect } from "./lib/effect"
import { agentHost, host } from "./plugin/host"
const it = testEffect(AgentV2.locationLayer)
const testLocation = location({ directory: AbsolutePath.make("/project") })
const locationLayer = Layer.succeed(Location.Service, Location.Service.of(testLocation))
const it = testEffect(
AgentV2.locationLayer.pipe(
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
),
)
describe("AgentV2", () => {
it.effect("publishes an updated event after agent changes", () =>
Effect.gen(function* () {
const agent = yield* AgentV2.Service
const events = yield* EventV2.Service
const updated = yield* events
.subscribe(AgentV2.Event.Updated)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
yield* agent.transform((editor) => editor.update(AgentV2.ID.make("reviewer"), () => {}))
expect(yield* Fiber.join(updated)).toMatchObject([{ location: { directory: testLocation.directory } }])
}),
)
it.effect("starts without agents", () =>
Effect.gen(function* () {
const agent = yield* AgentV2.Service

View file

@ -5,6 +5,7 @@ import { Effect, Layer, Schema } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Config } from "@opencode-ai/core/config"
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
import { EventV2 } from "@opencode-ai/core/event"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
@ -12,7 +13,9 @@ import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { agentHost, host } from "../plugin/host"
const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
const it = testEffect(
Layer.mergeAll(AgentV2.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer)), FSUtil.defaultLayer),
)
const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigAgentPlugin.Plugin", () => {

View file

@ -59,7 +59,7 @@ const permission = Layer.succeed(
}),
)
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const agents = AgentV2.layer
const agents = AgentV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
const model = OpenAIChat.route
.with({
endpoint: { baseURL: "https://api.openai.com/v1" },

View file

@ -129,7 +129,7 @@ const registry = ToolRegistry.layer.pipe(
Layer.provide(applications),
Layer.provide(ToolOutputStore.defaultLayer),
)
const agents = AgentV2.layer
const agents = AgentV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
const echo = Layer.effectDiscard(
ToolRegistry.Service.use((registry) =>
registry.register({