feat(tui): refresh agents after update events
This commit is contained in:
parent
4ce830a919
commit
02cb350880
17 changed files with 576 additions and 30 deletions
|
|
@ -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] })
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { Agent } from "@opencode-ai/schema"
|
||||
import { EventManifest as SchemaEventManifest } from "@opencode-ai/schema/event-manifest"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { EventManifest } from "@/event-manifest"
|
||||
|
|
@ -9,8 +10,9 @@ describe("public event manifest", () => {
|
|||
expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions)
|
||||
expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest)
|
||||
expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable)
|
||||
expect(EventManifest.Latest.size).toBe(88)
|
||||
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(93)
|
||||
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
|
||||
expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated)
|
||||
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
||||
expect(EventManifest.Latest.has("server.connected")).toBe(true)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
export * as Agent from "./agent"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { define, inventory } from "./event"
|
||||
import { optional } from "./schema"
|
||||
import { Model } from "./model"
|
||||
import { Permission } from "./permission"
|
||||
import { Provider } from "./provider"
|
||||
import { PositiveInt, statics } from "./schema"
|
||||
|
||||
const Updated = define({ type: "agent.updated", schema: {} })
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
|
|
@ -33,6 +36,20 @@ export const Info = Schema.Struct({
|
|||
.pipe(
|
||||
statics((schema) => ({
|
||||
empty: (id: ID) =>
|
||||
schema.make({ id, request: { headers: {}, body: {} }, mode: "all", hidden: false, permissions: [] }),
|
||||
schema.make({
|
||||
id,
|
||||
request: { headers: {}, body: {} },
|
||||
mode: "all",
|
||||
hidden: false,
|
||||
permissions: [
|
||||
{ action: "*", resource: "*", effect: "allow" },
|
||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||
],
|
||||
}),
|
||||
})),
|
||||
)
|
||||
|
||||
export const Event = {
|
||||
Updated,
|
||||
Definitions: inventory(Updated),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * as EventManifest from "./event-manifest"
|
||||
|
||||
import { Agent } from "./agent"
|
||||
import { Catalog } from "./catalog"
|
||||
import { Durable } from "./durable-event-manifest"
|
||||
import { Event } from "./event"
|
||||
|
|
@ -41,6 +42,7 @@ const foundationDefinitions = Event.inventory(
|
|||
...ModelsDev.Event.Definitions,
|
||||
...Integration.Event.Definitions,
|
||||
...Catalog.Event.Definitions,
|
||||
...Agent.Event.Definitions,
|
||||
...coreDefinitions,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { FileSystem, Integration, Permission, Project, Reference, Session, Workspace } from "../src"
|
||||
import { Agent, FileSystem, Integration, Permission, Project, Reference, Session, Workspace } from "../src"
|
||||
import { EventManifest } from "../src/event-manifest"
|
||||
import { IdeEvent } from "../src/ide-event"
|
||||
import { SessionEvent } from "../src/session-event"
|
||||
|
|
@ -9,8 +9,14 @@ import { WorkspaceEvent } from "../src/workspace-event"
|
|||
|
||||
describe("public event manifest", () => {
|
||||
test("owns the complete public event surface", () => {
|
||||
expect(EventManifest.ServerDefinitions.length).toBe(55)
|
||||
expect(EventManifest.Definitions.length).toBe(85)
|
||||
expect(EventManifest.ServerDefinitions.filter((definition) => definition.type !== "agent.updated").length).toBe(63)
|
||||
expect(EventManifest.ServerDefinitions.filter((definition) => definition.type === "agent.updated")).toEqual([
|
||||
Agent.Event.Updated,
|
||||
])
|
||||
expect(EventManifest.Definitions.filter((definition) => definition.type !== "agent.updated").length).toBe(93)
|
||||
expect(EventManifest.Definitions.filter((definition) => definition.type === "agent.updated")).toEqual([
|
||||
Agent.Event.Updated,
|
||||
])
|
||||
expect(SessionV1.Event.Definitions).toEqual([
|
||||
SessionV1.Event.Created,
|
||||
SessionV1.Event.Updated,
|
||||
|
|
@ -23,8 +29,10 @@ describe("public event manifest", () => {
|
|||
SessionV1.Event.Diff,
|
||||
SessionV1.Event.Error,
|
||||
])
|
||||
expect(EventManifest.Latest.size).toBe(85)
|
||||
expect(EventManifest.Durable.size).toBe(32)
|
||||
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(93)
|
||||
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
|
||||
expect(Agent.Event.Updated.durable).toBeUndefined()
|
||||
expect(EventManifest.Durable.has("agent.updated")).toBe(false)
|
||||
})
|
||||
|
||||
test("uses canonical definitions for current public events", () => {
|
||||
|
|
@ -34,7 +42,9 @@ describe("public event manifest", () => {
|
|||
expect(Workspace.Event.Definitions).toBe(WorkspaceEvent.Definitions)
|
||||
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
|
||||
expect(EventManifest.Latest.get("todo.updated")).toBe(SessionTodo.Event.Updated)
|
||||
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
|
||||
expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated)
|
||||
expect(Agent.Event.Definitions).toEqual([Agent.Event.Updated])
|
||||
expect(Project.Event.Definitions).toEqual([Project.Event.Updated])
|
||||
expect(FileSystem.Event.Definitions).toEqual([FileSystem.Event.Edited])
|
||||
expect(Integration.Event.Definitions).toEqual([Integration.Event.Updated, Integration.Event.ConnectionUpdated])
|
||||
|
|
@ -42,7 +52,8 @@ describe("public event manifest", () => {
|
|||
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
|
||||
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
|
||||
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
|
||||
expect(EventManifest.Definitions.slice(40, 43)).toEqual([
|
||||
const sessionV1TailStart = EventManifest.Definitions.indexOf(SessionV1.Event.PartDelta)
|
||||
expect(EventManifest.Definitions.slice(sessionV1TailStart, sessionV1TailStart + 3)).toEqual([
|
||||
SessionV1.Event.PartDelta,
|
||||
SessionV1.Event.Diff,
|
||||
SessionV1.Event.Error,
|
||||
|
|
|
|||
|
|
@ -279,6 +279,8 @@ import type {
|
|||
V2FsListResponses,
|
||||
V2FsReadErrors,
|
||||
V2FsReadResponses,
|
||||
V2GenerateTextErrors,
|
||||
V2GenerateTextResponses,
|
||||
V2HealthGetErrors,
|
||||
V2HealthGetResponses,
|
||||
V2IntegrationAttemptCancelErrors,
|
||||
|
|
@ -311,6 +313,10 @@ import type {
|
|||
V2ProjectCopyRefreshResponses,
|
||||
V2ProjectCopyRemoveErrors,
|
||||
V2ProjectCopyRemoveResponses,
|
||||
V2ProjectCurrentErrors,
|
||||
V2ProjectCurrentResponses,
|
||||
V2ProjectDirectoriesErrors,
|
||||
V2ProjectDirectoriesResponses,
|
||||
V2ProviderGetErrors,
|
||||
V2ProviderGetResponses,
|
||||
V2ProviderListErrors,
|
||||
|
|
@ -343,6 +349,8 @@ import type {
|
|||
V2SessionCreateResponses,
|
||||
V2SessionEventsErrors,
|
||||
V2SessionEventsResponses,
|
||||
V2SessionForkErrors,
|
||||
V2SessionForkResponses,
|
||||
V2SessionGetErrors,
|
||||
V2SessionGetResponses,
|
||||
V2SessionHistoryErrors,
|
||||
|
|
@ -5548,6 +5556,41 @@ export class Session3 extends HeyApiClient {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fork session
|
||||
*
|
||||
* Create a child session by copying projected history from the parent. When messageID is supplied, copy messages before that boundary.
|
||||
*/
|
||||
public fork<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
sessionID: string
|
||||
messageID?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "sessionID" },
|
||||
{ in: "body", key: "messageID" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<V2SessionForkResponses, V2SessionForkErrors, ThrowOnError>({
|
||||
url: "/api/session/{sessionID}/fork",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch session agent
|
||||
*
|
||||
|
|
@ -5944,6 +5987,48 @@ export class Model extends HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
export class Generate extends HeyApiClient {
|
||||
/**
|
||||
* Generate text
|
||||
*
|
||||
* Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified.
|
||||
*/
|
||||
public text<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
location?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
prompt?: string
|
||||
model?: ModelRef
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "location" },
|
||||
{ in: "body", key: "prompt" },
|
||||
{ in: "body", key: "model" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<V2GenerateTextResponses, V2GenerateTextErrors, ThrowOnError>({
|
||||
url: "/api/generate",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Provider2 extends HeyApiClient {
|
||||
/**
|
||||
* List providers
|
||||
|
|
@ -6363,6 +6448,67 @@ export class Credential extends HeyApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
export class Project2 extends HeyApiClient {
|
||||
/**
|
||||
* Get current project
|
||||
*
|
||||
* Resolve the project for the requested location.
|
||||
*/
|
||||
public current<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
location?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
|
||||
return (options?.client ?? this.client).get<V2ProjectCurrentResponses, V2ProjectCurrentErrors, ThrowOnError>({
|
||||
url: "/api/project/current",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List project directories
|
||||
*
|
||||
* List known local absolute directories for a project.
|
||||
*/
|
||||
public directories<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
projectID: string
|
||||
location?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "projectID" },
|
||||
{ in: "query", key: "location" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<
|
||||
V2ProjectDirectoriesResponses,
|
||||
V2ProjectDirectoriesErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/project/{projectID}/directories",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Request extends HeyApiClient {
|
||||
/**
|
||||
* List pending permission requests
|
||||
|
|
@ -7233,6 +7379,11 @@ export class V2 extends HeyApiClient {
|
|||
return (this._model ??= new Model({ client: this.client }))
|
||||
}
|
||||
|
||||
private _generate?: Generate
|
||||
get generate(): Generate {
|
||||
return (this._generate ??= new Generate({ client: this.client }))
|
||||
}
|
||||
|
||||
private _provider?: Provider2
|
||||
get provider(): Provider2 {
|
||||
return (this._provider ??= new Provider2({ client: this.client }))
|
||||
|
|
@ -7248,6 +7399,11 @@ export class V2 extends HeyApiClient {
|
|||
return (this._credential ??= new Credential({ client: this.client }))
|
||||
}
|
||||
|
||||
private _project?: Project2
|
||||
get project(): Project2 {
|
||||
return (this._project ??= new Project2({ client: this.client }))
|
||||
}
|
||||
|
||||
private _permission?: Permission3
|
||||
get permission(): Permission3 {
|
||||
return (this._permission ??= new Permission3({ client: this.client }))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type Event =
|
|||
| EventIntegrationUpdated
|
||||
| EventIntegrationConnectionUpdated
|
||||
| EventCatalogUpdated
|
||||
| EventAgentUpdated
|
||||
| EventSessionCreated
|
||||
| EventSessionUpdated
|
||||
| EventSessionDeleted
|
||||
|
|
@ -20,6 +21,7 @@ export type Event =
|
|||
| EventSessionNextModelSwitched
|
||||
| EventSessionNextMoved
|
||||
| EventSessionNextRenamed
|
||||
| EventSessionNextForked
|
||||
| EventSessionNextPrompted
|
||||
| EventSessionNextPromptAdmitted
|
||||
| EventSessionNextContextUpdated
|
||||
|
|
@ -646,7 +648,6 @@ export type Prompt = {
|
|||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
system?: string
|
||||
}
|
||||
|
||||
export type Pty = {
|
||||
|
|
@ -783,6 +784,13 @@ export type GlobalEvent = {
|
|||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "agent.updated"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.created"
|
||||
|
|
@ -880,6 +888,16 @@ export type GlobalEvent = {
|
|||
title: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.next.forked"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
parentID: string
|
||||
messageID?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "session.next.prompted"
|
||||
|
|
@ -1667,6 +1685,7 @@ export type GlobalEvent = {
|
|||
| SyncEventSessionNextModelSwitched
|
||||
| SyncEventSessionNextMoved
|
||||
| SyncEventSessionNextRenamed
|
||||
| SyncEventSessionNextForked
|
||||
| SyncEventSessionNextPrompted
|
||||
| SyncEventSessionNextPromptAdmitted
|
||||
| SyncEventSessionNextContextUpdated
|
||||
|
|
@ -2756,11 +2775,17 @@ export type SessionNotFoundError = {
|
|||
message: string
|
||||
}
|
||||
|
||||
export type MessageNotFoundError = {
|
||||
_tag: "MessageNotFoundError"
|
||||
sessionID: string
|
||||
messageID: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type PromptInput = {
|
||||
text: string
|
||||
files?: Array<PromptInputFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
system?: string
|
||||
}
|
||||
|
||||
export type ConflictError = {
|
||||
|
|
@ -2781,18 +2806,12 @@ export type UnknownError1 = {
|
|||
ref?: string
|
||||
}
|
||||
|
||||
export type MessageNotFoundError = {
|
||||
_tag: "MessageNotFoundError"
|
||||
sessionID: string
|
||||
messageID: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export type SessionDurableEvent =
|
||||
| SessionNextAgentSwitched
|
||||
| SessionNextModelSwitched
|
||||
| SessionNextMoved
|
||||
| SessionNextRenamed
|
||||
| SessionNextForked
|
||||
| SessionNextPrompted
|
||||
| SessionNextPromptAdmitted
|
||||
| SessionNextContextUpdated
|
||||
|
|
@ -2834,6 +2853,12 @@ export type SessionMessagesResponse = {
|
|||
}
|
||||
}
|
||||
|
||||
export type GenerateTextResponse = {
|
||||
data: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ProviderNotFoundError = {
|
||||
_tag: "ProviderNotFoundError"
|
||||
providerID: string
|
||||
|
|
@ -2928,6 +2953,7 @@ export type V2Event =
|
|||
| IntegrationUpdated
|
||||
| IntegrationConnectionUpdated
|
||||
| CatalogUpdated
|
||||
| AgentUpdated
|
||||
| SessionCreated
|
||||
| SessionUpdated
|
||||
| SessionDeleted
|
||||
|
|
@ -2939,6 +2965,7 @@ export type V2Event =
|
|||
| SessionNextModelSwitched
|
||||
| SessionNextMoved
|
||||
| SessionNextRenamed
|
||||
| SessionNextForked
|
||||
| SessionNextPrompted
|
||||
| SessionNextPromptAdmitted
|
||||
| SessionNextContextUpdated
|
||||
|
|
@ -3464,6 +3491,23 @@ export type SyncEventSessionNextRenamed = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextForked = {
|
||||
type: "sync"
|
||||
id: string
|
||||
syncEvent: {
|
||||
type: "session.next.forked.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: string
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
parentID: string
|
||||
messageID?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextPrompted = {
|
||||
type: "sync"
|
||||
id: string
|
||||
|
|
@ -3959,10 +4003,12 @@ export type ConfigV2ExperimentalPolicy = {
|
|||
resource: string
|
||||
}
|
||||
|
||||
export type ProjectDirectories = Array<{
|
||||
export type ProjectDirectory = {
|
||||
directory: string
|
||||
strategy?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type ProjectDirectories = Array<ProjectDirectory>
|
||||
|
||||
export type PtyTicketConnectToken = {
|
||||
ticket: string
|
||||
|
|
@ -4096,7 +4142,6 @@ export type SessionMessageUser = {
|
|||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
system?: string
|
||||
type: "user"
|
||||
}
|
||||
|
||||
|
|
@ -4357,6 +4402,26 @@ export type SessionNextRenamed = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SessionNextForked = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type: "session.next.forked"
|
||||
durable?: {
|
||||
aggregateID: string
|
||||
seq: number
|
||||
version: number
|
||||
}
|
||||
location?: LocationRef
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
parentID: string
|
||||
messageID?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionNextPrompted = {
|
||||
id: string
|
||||
metadata?: {
|
||||
|
|
@ -5115,6 +5180,11 @@ export type IntegrationAttemptStatus =
|
|||
}
|
||||
}
|
||||
|
||||
export type ProjectCurrent = {
|
||||
id: string
|
||||
directory: string
|
||||
}
|
||||
|
||||
export type PermissionV2Request = {
|
||||
id: string
|
||||
sessionID: string
|
||||
|
|
@ -5224,6 +5294,23 @@ export type CatalogUpdated = {
|
|||
}
|
||||
}
|
||||
|
||||
export type AgentUpdated = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
type: "agent.updated"
|
||||
durable?: {
|
||||
aggregateID: string
|
||||
seq: number
|
||||
version: number
|
||||
}
|
||||
location?: LocationRef
|
||||
data: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionCreated = {
|
||||
id: string
|
||||
metadata?: {
|
||||
|
|
@ -6371,6 +6458,14 @@ export type EventCatalogUpdated = {
|
|||
}
|
||||
}
|
||||
|
||||
export type EventAgentUpdated = {
|
||||
id: string
|
||||
type: "agent.updated"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionCreated = {
|
||||
id: string
|
||||
type: "session.created"
|
||||
|
|
@ -6479,6 +6574,17 @@ export type EventSessionNextRenamed = {
|
|||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextForked = {
|
||||
id: string
|
||||
type: "session.next.forked"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
parentID: string
|
||||
messageID?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextPrompted = {
|
||||
id: string
|
||||
type: "session.next.prompted"
|
||||
|
|
@ -11700,6 +11806,45 @@ export type V2SessionGetResponses = {
|
|||
|
||||
export type V2SessionGetResponse = V2SessionGetResponses[keyof V2SessionGetResponses]
|
||||
|
||||
export type V2SessionForkData = {
|
||||
body: {
|
||||
messageID?: string
|
||||
}
|
||||
path: {
|
||||
sessionID: string
|
||||
}
|
||||
query?: never
|
||||
url: "/api/session/{sessionID}/fork"
|
||||
}
|
||||
|
||||
export type V2SessionForkErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
/**
|
||||
* SessionNotFoundError | MessageNotFoundError
|
||||
*/
|
||||
404: MessageNotFoundError | SessionNotFoundError
|
||||
}
|
||||
|
||||
export type V2SessionForkError = V2SessionForkErrors[keyof V2SessionForkErrors]
|
||||
|
||||
export type V2SessionForkResponses = {
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
200: {
|
||||
data: SessionV2Info
|
||||
}
|
||||
}
|
||||
|
||||
export type V2SessionForkResponse = V2SessionForkResponses[keyof V2SessionForkResponses]
|
||||
|
||||
export type V2SessionSwitchAgentData = {
|
||||
body: {
|
||||
agent: string
|
||||
|
|
@ -12353,6 +12498,47 @@ export type V2ModelListResponses = {
|
|||
|
||||
export type V2ModelListResponse = V2ModelListResponses[keyof V2ModelListResponses]
|
||||
|
||||
export type V2GenerateTextData = {
|
||||
body: {
|
||||
prompt: string
|
||||
model?: ModelRef
|
||||
}
|
||||
path?: never
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
}
|
||||
url: "/api/generate"
|
||||
}
|
||||
|
||||
export type V2GenerateTextErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
/**
|
||||
* ServiceUnavailableError
|
||||
*/
|
||||
503: ServiceUnavailableError
|
||||
}
|
||||
|
||||
export type V2GenerateTextError = V2GenerateTextErrors[keyof V2GenerateTextErrors]
|
||||
|
||||
export type V2GenerateTextResponses = {
|
||||
/**
|
||||
* GenerateTextResponse
|
||||
*/
|
||||
200: GenerateTextResponse
|
||||
}
|
||||
|
||||
export type V2GenerateTextResponse = V2GenerateTextResponses[keyof V2GenerateTextResponses]
|
||||
|
||||
export type V2ProviderListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
|
@ -12793,6 +12979,76 @@ export type V2CredentialUpdateResponses = {
|
|||
|
||||
export type V2CredentialUpdateResponse = V2CredentialUpdateResponses[keyof V2CredentialUpdateResponses]
|
||||
|
||||
export type V2ProjectCurrentData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
}
|
||||
url: "/api/project/current"
|
||||
}
|
||||
|
||||
export type V2ProjectCurrentErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
}
|
||||
|
||||
export type V2ProjectCurrentError = V2ProjectCurrentErrors[keyof V2ProjectCurrentErrors]
|
||||
|
||||
export type V2ProjectCurrentResponses = {
|
||||
/**
|
||||
* Project.Current
|
||||
*/
|
||||
200: ProjectCurrent
|
||||
}
|
||||
|
||||
export type V2ProjectCurrentResponse = V2ProjectCurrentResponses[keyof V2ProjectCurrentResponses]
|
||||
|
||||
export type V2ProjectDirectoriesData = {
|
||||
body?: never
|
||||
path: {
|
||||
projectID: string
|
||||
}
|
||||
query?: {
|
||||
location?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
}
|
||||
url: "/api/project/{projectID}/directories"
|
||||
}
|
||||
|
||||
export type V2ProjectDirectoriesErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
}
|
||||
|
||||
export type V2ProjectDirectoriesError = V2ProjectDirectoriesErrors[keyof V2ProjectDirectoriesErrors]
|
||||
|
||||
export type V2ProjectDirectoriesResponses = {
|
||||
/**
|
||||
* Project.Directories
|
||||
*/
|
||||
200: ProjectDirectories
|
||||
}
|
||||
|
||||
export type V2ProjectDirectoriesResponse = V2ProjectDirectoriesResponses[keyof V2ProjectDirectoriesResponses]
|
||||
|
||||
export type V2PermissionRequestListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
result.location.provider.refresh(event.location),
|
||||
])
|
||||
break
|
||||
case "agent.updated":
|
||||
void result.location.agent.refresh(event.location)
|
||||
break
|
||||
case "session.next.agent.switched":
|
||||
if (store.session.info[event.data.sessionID])
|
||||
setStore("session", "info", event.data.sessionID, "agent", event.data.agent)
|
||||
|
|
|
|||
|
|
@ -170,9 +170,22 @@ export function Session() {
|
|||
})
|
||||
onCleanup(() => setEpilogue())
|
||||
const messages = sessionMessages
|
||||
const descendantSessionIDs = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
const sessions = data.session.list()
|
||||
const childrenByParent = sessions.reduce((acc, item) => {
|
||||
if (!item.parentID) return acc
|
||||
acc.set(item.parentID, [...(acc.get(item.parentID) ?? []), item.id])
|
||||
return acc
|
||||
}, new Map<string, string[]>())
|
||||
function collect(sessionID: string): string[] {
|
||||
return (childrenByParent.get(sessionID) ?? []).flatMap((id) => [id, ...collect(id)])
|
||||
}
|
||||
return collect(route.sessionID)
|
||||
})
|
||||
const permissions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return data.session.permission.list(route.sessionID) ?? []
|
||||
return [route.sessionID, ...descendantSessionIDs()].flatMap((sessionID) => data.session.permission.list(sessionID) ?? [])
|
||||
})
|
||||
const questions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
|
|
@ -227,6 +240,12 @@ export function Session() {
|
|||
const editor = useEditorContext()
|
||||
const rows = createSessionRows(() => route.sessionID)
|
||||
|
||||
createEffect(
|
||||
on(descendantSessionIDs, (sessionIDs) => {
|
||||
void Promise.all(sessionIDs.map((sessionID) => data.session.permission.refresh(sessionID)))
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const sessionID = route.sessionID
|
||||
void (async () => {
|
||||
|
|
|
|||
|
|
@ -356,6 +356,53 @@ test("refreshes effective catalog data after catalog updates", async () => {
|
|||
}
|
||||
})
|
||||
|
||||
test("refreshes agents after agent updates", async () => {
|
||||
const events = createEventStream()
|
||||
let requests = 0
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname !== "/api/agent") return
|
||||
requests++
|
||||
return json({
|
||||
location: { directory, project: { id: "proj_test", directory } },
|
||||
data: [
|
||||
{
|
||||
id: requests === 1 ? "build" : "reviewer",
|
||||
request: { headers: {}, body: {} },
|
||||
mode: "primary",
|
||||
hidden: false,
|
||||
permissions: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
return <box />
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<Probe />
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
await wait(() => data.location.agent.list()?.[0]?.id === "build")
|
||||
emitEvent(events, { id: "evt_agent", type: "agent.updated", data: {} })
|
||||
await wait(() => data.location.agent.list()?.[0]?.id === "reviewer")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("refreshes references after updates", async () => {
|
||||
const events = createEventStream()
|
||||
let requests = 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue