refactor(tui): wire generated client reads (#34381)

This commit is contained in:
Kit Langton 2026-06-29 11:36:10 -04:00 committed by GitHub
commit 381d67572e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 509 additions and 427 deletions

View file

@ -4,7 +4,7 @@ import { createTestRenderer } from "@opentui/core/testing"
import { Effect } from "effect"
import { Global } from "@opencode-ai/core/global"
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
import { createClient, createEventStream, createFetch, directory, json } from "./fixture/tui-sdk"
import { createApi, createClient, createEventStream, createFetch, directory, json } from "./fixture/tui-sdk"
test("SIGHUP clears title and disposes scoped resources once", async () => {
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
@ -30,6 +30,7 @@ test("SIGHUP clears title and disposes scoped resources once", async () => {
const task = Effect.runPromise(
run({
client: createClient(calls.fetch),
api: createApi(calls.fetch),
config: createTuiResolvedConfig({ plugin_enabled: {} }),
args: {},
pluginHost: {
@ -61,26 +62,23 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
const core = await import("@opentui/core")
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
const events = createEventStream()
const calls = createFetch(
(url) => {
if (url.pathname === "/api/session")
return json({
data: [
{
id: "dummy",
title: "Demo session",
projectID: "project",
location: { directory },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
},
],
cursor: {},
})
},
events,
)
const calls = createFetch((url) => {
if (url.pathname === "/api/session")
return json({
data: [
{
id: "dummy",
title: "Demo session",
projectID: "project",
location: { directory },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
},
],
cursor: {},
})
}, events)
const originalWrite = process.stdout.write.bind(process.stdout)
let stdout = ""
let api: TuiPluginApi | undefined
@ -99,6 +97,7 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => {
const task = Effect.runPromise(
run({
client: createClient(calls.fetch),
api: createApi(calls.fetch),
config: createTuiResolvedConfig({ plugin_enabled: {} }),
args: { continue: true },
pluginHost: {

View file

@ -7,7 +7,7 @@ import { ProjectProvider, useProject } from "../../../../src/context/project"
import { SDKProvider } from "../../../../src/context/sdk"
import { SyncProvider, useSync } from "../../../../src/context/sync"
import { ExitProvider } from "../../../../src/context/exit"
import { createClient, createEventStream, createFetch, type FetchHandler } from "../../../fixture/tui-sdk"
import { createApi, createClient, createEventStream, createFetch, type FetchHandler } from "../../../fixture/tui-sdk"
import { TestTuiContexts } from "../../../fixture/tui-environment"
export { createEventStream, createFetch, directory, json, worktree } from "../../../fixture/tui-sdk"
@ -47,7 +47,7 @@ export async function mount(override?: FetchHandler, state?: string) {
<TestTuiContexts paths={state ? { state } : undefined}>
<ArgsProvider>
<KVProvider>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<ExitProvider exit={() => {}}>
<SyncProvider>

View file

@ -6,7 +6,7 @@ import { onMount } from "solid-js"
import { ProjectProvider } from "../../../src/context/project"
import { SDKProvider } from "../../../src/context/sdk"
import { DataProvider, useData } from "../../../src/context/data"
import { createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
import { TestTuiContexts } from "../../fixture/tui-environment"
async function wait(fn: () => boolean, timeout = 2000) {
@ -69,7 +69,7 @@ test("refreshes resources into reactive getters", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -139,7 +139,7 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -172,8 +172,7 @@ test("reconnects the event stream and bootstraps fresh data", async () => {
test("tracks session status from active sessions and execution events", async () => {
const events = createEventStream()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/active")
return json({ data: { "session-active": { type: "running" } } })
if (url.pathname === "/api/session/active") return json({ data: { "session-active": { type: "running" } } })
}, events)
let data!: ReturnType<typeof useData>
@ -184,7 +183,7 @@ test("tracks session status from active sessions and execution events", async ()
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -296,7 +295,7 @@ test("refreshes integrations after integration updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -337,7 +336,7 @@ test("refreshes effective catalog data after catalog updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<box />
@ -382,7 +381,7 @@ test("refreshes references after updates", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -415,7 +414,7 @@ test("adds and dismisses permission requests from live events", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -480,7 +479,7 @@ test("adds and dismisses question requests from live events", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -548,7 +547,7 @@ test("settles pending tools when a live failure arrives", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -677,7 +676,7 @@ test("renders admitted prompts only after they become model-visible", async () =
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
@ -750,7 +749,7 @@ test("projects live context updates with their message ID", async () => {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />

View file

@ -1,12 +1,13 @@
/** @jsxImportSource @opentui/solid */
import { describe, expect, test } from "bun:test"
import type { OpenCodeClient } from "@opencode-ai/client"
import { testRender } from "@opentui/solid"
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
import { onMount } from "solid-js"
import { ProjectProvider, useProject } from "../../../src/context/project"
import { SDKProvider, useSDK } from "../../../src/context/sdk"
import { useEvent } from "../../../src/context/event"
import { createClient, createEventStream, createFetch, directory } from "../../fixture/tui-sdk"
import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk"
import { TestTuiContexts } from "../../fixture/tui-environment"
const projectID = "proj_test"
@ -46,7 +47,7 @@ function update(version: string): V2Event {
}
}
async function mount(reload?: () => Promise<OpencodeClient>) {
async function mount(reload?: () => Promise<{ client: OpencodeClient; api: OpenCodeClient }>) {
const events = createEventStream()
const calls = createFetch(undefined, events)
const seen: V2Event[] = []
@ -60,7 +61,7 @@ async function mount(reload?: () => Promise<OpencodeClient>) {
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} reload={reload}>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)} reload={reload}>
<ProjectProvider>
<Probe
onReady={async (ctx) => {
@ -150,7 +151,8 @@ describe("useEvent", () => {
test("reloads the host and reconnects the event stream", async () => {
let calls = 0
const events = createEventStream()
const replacement = createClient(createFetch(undefined, events).fetch)
const replacementCalls = createFetch(undefined, events)
const replacement = { client: createClient(replacementCalls.fetch), api: createApi(replacementCalls.fetch) }
const { app, sdk, seen } = await mount(async () => {
calls += 1
return replacement
@ -164,19 +166,21 @@ describe("useEvent", () => {
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "reloaded"))
expect(calls).toBe(1)
expect(sdk.client).toBe(replacement)
expect(sdk.client).toBe(replacement.client)
expect(sdk.api).toBe(replacement.api)
} finally {
app.renderer.destroy()
}
})
test("keeps the current event stream alive while the host reload is pending", async () => {
let complete!: (client: OpencodeClient) => void
const pending = new Promise<OpencodeClient>((resolve) => {
let complete!: (client: { client: OpencodeClient; api: OpenCodeClient }) => void
const pending = new Promise<{ client: OpencodeClient; api: OpenCodeClient }>((resolve) => {
complete = resolve
})
const replacementEvents = createEventStream()
const replacement = createClient(createFetch(undefined, replacementEvents).fetch)
const replacementCalls = createFetch(undefined, replacementEvents)
const replacement = { client: createClient(replacementCalls.fetch), api: createApi(replacementCalls.fetch) }
const { app, emit, sdk, seen } = await mount(() => pending)
try {
@ -188,7 +192,8 @@ describe("useEvent", () => {
expect(sdk.connection.status()).toBe("connected")
complete(replacement)
await reload
expect(sdk.client).toBe(replacement)
expect(sdk.client).toBe(replacement.client)
expect(sdk.api).toBe(replacement.api)
} finally {
app.renderer.destroy()
}

View file

@ -1,3 +1,4 @@
import { OpenCode } from "@opencode-ai/client"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import type { V2Event } from "@opencode-ai/sdk/v2"
@ -117,3 +118,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType<typeof
export function createClient(fetch: typeof globalThis.fetch) {
return createOpencodeClient({ baseUrl: "http://test", fetch })
}
export function createApi(fetch: typeof globalThis.fetch) {
return OpenCode.make({ baseUrl: "http://test", fetch })
}