From 0fa9e5039e3af6bd8dbcbad54b916fa172a01193 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 11 Jul 2026 17:05:23 -0400 Subject: [PATCH] refactor(tui): reduce legacy sync usage --- packages/client/src/effect/api/api.ts | 14 +-- .../client/src/effect/generated/client.ts | 2 +- .../client/src/promise/generated/client.ts | 18 +-- .../client/src/promise/generated/types.ts | 8 +- packages/protocol/src/client.ts | 1 + packages/tui/src/app.tsx | 14 +-- packages/tui/src/component/dialog-mcp.tsx | 2 +- packages/tui/src/component/dialog-status.tsx | 103 +----------------- .../tui/src/component/prompt/autocomplete.tsx | 6 +- packages/tui/src/component/prompt/index.tsx | 4 +- packages/tui/src/context/data.tsx | 44 ++++++-- packages/tui/src/context/directory.ts | 6 +- packages/tui/src/context/local.tsx | 19 +--- packages/tui/src/plugin/adapters.tsx | 24 ++-- packages/tui/src/routes/home.tsx | 6 +- .../src/routes/home/session-destination.tsx | 6 +- packages/tui/src/routes/session/footer.tsx | 14 +-- packages/tui/test/cli/tui/data.test.tsx | 64 +++++++++++ packages/tui/test/fixture/tui-sdk.ts | 5 + 19 files changed, 166 insertions(+), 194 deletions(-) diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 334c4b1f18..28c06cbf04 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -476,18 +476,16 @@ export interface IntegrationApi { type Endpoint11_0Request = Parameters[0] export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } export type Endpoint11_0Output = EffectValue> -export type ServerMcpListOperation = (input?: Endpoint11_0Input) => Effect.Effect +export type McpListOperation = (input?: Endpoint11_0Input) => Effect.Effect type Endpoint11_1Request = Parameters[0] export type Endpoint11_1Input = { readonly location?: Endpoint11_1Request["query"]["location"] } export type Endpoint11_1Output = EffectValue> -export type ServerMcpResourceCatalogOperation = ( - input?: Endpoint11_1Input, -) => Effect.Effect +export type McpResourceCatalogOperation = (input?: Endpoint11_1Input) => Effect.Effect -export interface ServerMcpApi { - readonly list: ServerMcpListOperation - readonly resource: { readonly catalog: ServerMcpResourceCatalogOperation } +export interface McpApi { + readonly list: McpListOperation + readonly resource: { readonly catalog: McpResourceCatalogOperation } } type Endpoint12_0Request = Parameters[0] @@ -955,7 +953,7 @@ export interface AppApi { readonly generate: GenerateApi readonly provider: ProviderApi readonly integration: IntegrationApi - readonly "server.mcp": ServerMcpApi + readonly mcp: McpApi readonly credential: CredentialApi readonly project: ProjectApi readonly form: FormApi diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 7e14e5151f..1f58717113 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -1134,7 +1134,7 @@ const adaptClient = (raw: RawClient) => ({ generate: adaptGroup8(raw["server.generate"]), provider: adaptGroup9(raw["server.provider"]), integration: adaptGroup10(raw["server.integration"]), - "server.mcp": adaptGroup11(raw["server.mcp"]), + mcp: adaptGroup11(raw["server.mcp"]), credential: adaptGroup12(raw["server.credential"]), project: adaptGroup13(raw["server.project"]), form: adaptGroup14(raw["server.form"]), diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index bdfd5958fc..bed325d976 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -90,10 +90,10 @@ import type { IntegrationAttemptCompleteOutput, IntegrationAttemptCancelInput, IntegrationAttemptCancelOutput, - ServerMcpListInput, - ServerMcpListOutput, - ServerMcpResourceCatalogInput, - ServerMcpResourceCatalogOutput, + McpListInput, + McpListOutput, + McpResourceCatalogInput, + McpResourceCatalogOutput, CredentialUpdateInput, CredentialUpdateOutput, CredentialRemoveInput, @@ -941,9 +941,9 @@ export function make(options: ClientOptions) { ), }, }, - "server.mcp": { - list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) => - request( + mcp: { + list: (input?: McpListInput, requestOptions?: RequestOptions) => + request( { method: "GET", path: `/api/mcp`, @@ -955,8 +955,8 @@ export function make(options: ClientOptions) { requestOptions, ), resource: { - catalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) => - request( + catalog: (input?: McpResourceCatalogInput, requestOptions?: RequestOptions) => + request( { method: "GET", path: `/api/mcp/resource`, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 85720c5798..255beaa1bb 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -3346,24 +3346,24 @@ export type IntegrationAttemptCancelInput = { export type IntegrationAttemptCancelOutput = void -export type ServerMcpListInput = { +export type McpListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined }["location"] } -export type ServerMcpListOutput = { +export type McpListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } data: Array } -export type ServerMcpResourceCatalogInput = { +export type McpResourceCatalogInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined }["location"] } -export type ServerMcpResourceCatalogOutput = { +export type McpResourceCatalogOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } data: McpResourceCatalog } diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index 27201783cd..e89525c1de 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -54,6 +54,7 @@ export const groupNames = { "server.event": "event", "server.pty": "pty", "server.shell": "shell", + "server.mcp": "mcp", "server.question": "question", "server.reference": "reference", "server.project": "project", diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index b76935586b..bd7d33c2c3 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -418,10 +418,10 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi const keymap = useOpencodeKeymap() const event = useEvent() const sdk = useSDK() + const sync = useSync() const toast = useToast() const themeState = useTheme() const { theme, mode, setMode, locked, lock, unlock } = themeState - const sync = useSync() const data = useData() const project = useProject() const exit = useExit() @@ -435,7 +435,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi // the same problem on every refresh while still re-alerting if the state changes. const mcpAlerted: Record = {} createEffect(() => { - for (const server of data.location.mcp.list() ?? []) { + for (const server of data.location.mcp.server.list() ?? []) { const status = server.status if (status.status !== "failed" && status.status !== "needs_auth") { delete mcpAlerted[server.name] @@ -520,7 +520,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi } const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal( - kv.get("paste_summary_enabled", !sync.data.config.experimental?.disable_paste_summary), + kv.get("paste_summary_enabled", true), ) // Update terminal window title based on current route and session @@ -574,7 +574,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi let continued = false createEffect(() => { - if (continued || sync.status === "loading" || !args.continue) return + if (continued || !args.continue) return continued = true const location = data.location.default() void sdk.api.session @@ -600,12 +600,10 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi .catch(toast.error) }) - // Handle --session with --fork: wait for sync to be fully complete before forking - // (session list loads in non-blocking phase for --session, so we must wait for "complete" - // to avoid a race where reconcile overwrites the newly forked session) + // Handle --session with --fork once. let forked = false createEffect(() => { - if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return + if (forked || !args.sessionID || !args.fork) return forked = true void sdk.api.session .fork({ sessionID: args.sessionID }) diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index 5702759159..0ef74bd551 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/packages/tui/src/component/dialog-mcp.tsx @@ -45,7 +45,7 @@ export function DialogMcp() { const servers = createMemo(() => pipe( - data.location.mcp.list() ?? [], + data.location.mcp.server.list() ?? [], sortBy( (server) => statusMeta(server.status, theme).rank, (server) => server.name, diff --git a/packages/tui/src/component/dialog-status.tsx b/packages/tui/src/component/dialog-status.tsx index 1446dfc4ae..3847bae3c7 100644 --- a/packages/tui/src/component/dialog-status.tsx +++ b/packages/tui/src/component/dialog-status.tsx @@ -1,48 +1,17 @@ import { TextAttributes } from "@opentui/core" -import { fileURLToPath } from "bun" import { useTheme } from "../context/theme" import { useDialog } from "../ui/dialog" -import { useSync } from "../context/sync" import { useData } from "../context/data" import { For, Match, Switch, Show, createMemo } from "solid-js" export type DialogStatusProps = {} export function DialogStatus() { - const sync = useSync() const data = useData() const { theme } = useTheme() const dialog = useDialog() - const mcp = createMemo(() => data.location.mcp.list() ?? []) - const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) - - const plugins = createMemo(() => { - const list = sync.data.config.plugin ?? [] - const result = list.map((item) => { - const value = typeof item === "string" ? item : item[0] - if (value.startsWith("file://")) { - const path = fileURLToPath(value) - const parts = path.split("/") - const filename = parts.pop() || path - if (!filename.includes(".")) return { name: filename } - const basename = filename.split(".")[0] - if (basename === "index") { - const dirname = parts.pop() - const name = dirname || basename - return { name } - } - return { name: basename } - } - const index = value.lastIndexOf("@") - if (index <= 0) return { name: value, version: "latest" } - const name = value.substring(0, index) - const version = value.substring(index + 1) - return { name, version } - }) - return result.toSorted((a, b) => a.name.localeCompare(b.name)) - }) - + const mcp = createMemo(() => data.location.mcp.server.list() ?? []) return ( @@ -94,76 +63,6 @@ export function DialogStatus() { - {sync.data.lsp.length > 0 && ( - - {sync.data.lsp.length} LSP Servers - - {(item) => ( - - - • - - - {item.id} {item.root} - - - )} - - - )} - 0} fallback={No Formatters}> - - {enabledFormatters().length} Formatters - - {(item) => ( - - - • - - - {item.name} - - - )} - - - - 0} fallback={No Plugins}> - - {plugins().length} Plugins - - {(item) => ( - - - • - - - {item.name} - {item.version && @{item.version}} - - - )} - - - ) } diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 9b79590d5a..91b4bbdb42 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -8,7 +8,6 @@ import { createStore } from "solid-js/store" import { useEditorContext } from "../../context/editor" import { useProject } from "../../context/project" import { useSDK } from "../../context/sdk" -import { useSync } from "../../context/sync" import { useData } from "../../context/data" import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" @@ -86,7 +85,6 @@ export function Autocomplete(props: { }) { const editor = useEditorContext() const sdk = useSDK() - const sync = useSync() const data = useData() const project = useProject() const slashes = useCommandSlashes() @@ -285,7 +283,7 @@ export function Autocomplete(props: { }) function normalizeMentionPath(filePath: string) { - const baseDir = location()?.directory || sync.path.directory || paths.cwd + const baseDir = location()?.directory || project.instance.directory() || paths.cwd const absolute = path.resolve(filePath) const relative = path.relative(baseDir, absolute) @@ -363,7 +361,7 @@ export function Autocomplete(props: { const options: AutocompleteOption[] = [] const width = props.anchor().width - 4 - for (const res of Object.values(sync.data.mcp_resource)) { + for (const res of data.location.mcp.resource.list(location()) ?? []) { options.push({ display: Locale.truncateMiddle(res.name, width), // Match the name only; matching the URI caused unrelated fuzzy hits. diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 3ded3161a3..54a9d1b70c 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -23,7 +23,6 @@ import { Spinner } from "../spinner" import { useSDK } from "../../context/sdk" import { useRoute } from "../../context/route" import { useProject } from "../../context/project" -import { useSync } from "../../context/sync" import { useEvent } from "../../context/event" import { editorSelectionKey, useEditorContext, type EditorSelection } from "../../context/editor" import { normalizePromptContent, openEditor } from "../../editor" @@ -153,7 +152,6 @@ export function Prompt(props: PromptProps) { const editor = useEditorContext() const route = useRoute() const project = useProject() - const sync = useSync() const data = useData() const currentLocation = useLocation() const tuiConfig = useTuiConfig() @@ -1193,7 +1191,7 @@ export function Prompt(props: PromptProps) { const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1 if ( (lineCount >= 3 || pastedContent.length > 150) && - kv.get("paste_summary_enabled", !sync.data.config.experimental?.disable_paste_summary) + kv.get("paste_summary_enabled", true) ) { pasteText(pastedContent, `[Pasted ~${lineCount} lines]`) return diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index d714c62edb..601db51ddd 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -9,6 +9,7 @@ import type { FormInfo, IntegrationInfo, LocationRef, + McpResource, McpServer, ModelInfo, PermissionSavedInfo, @@ -43,7 +44,10 @@ type LocationData = { agent?: AgentInfo[] command?: CommandInfo[] integration?: IntegrationInfo[] - mcp?: McpServer[] + mcp?: { + server?: McpServer[] + resource?: McpResource[] + } model?: ModelInfo[] provider?: ProviderV2Info[] reference?: ReferenceInfo[] @@ -801,7 +805,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ // so the mcp list refreshes here rather than off integration.updated. case "mcp.status.changed": if (bootstrapping) break - void result.location.mcp.refresh(event.location) + void result.location.mcp.server.refresh(event.location) + break + case "mcp.resources.changed": + void result.location.mcp.resource.refresh(event.location) break } } @@ -987,13 +994,31 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }, }, mcp: { - list(location?: LocationRef) { - return store.location[locationKey(location ?? defaultLocation())]?.mcp + server: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.mcp?.server + }, + async refresh(ref?: LocationRef) { + const result = await sdk.api.mcp.list({ location: locationQuery(ref) }) + const key = locationKey(result.location) + setStore("location", key, { + ...store.location[key], + mcp: { ...store.location[key]?.mcp, server: result.data }, + }) + }, }, - async refresh(ref?: LocationRef) { - const result = await sdk.api["server.mcp"].list({ location: locationQuery(ref) }) - const key = locationKey(result.location) - setStore("location", key, { ...store.location[key], mcp: result.data }) + resource: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.mcp?.resource + }, + async refresh(ref?: LocationRef) { + const result = await sdk.api.mcp.resource.catalog({ location: locationQuery(ref) }) + const key = locationKey(result.location) + setStore("location", key, { + ...store.location[key], + mcp: { ...store.location[key]?.mcp, resource: result.data.resources }, + }) + }, }, }, model: { @@ -1089,7 +1114,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ result.location.refresh(), result.location.agent.refresh(), result.location.integration.refresh(), - result.location.mcp.refresh(), + result.location.mcp.server.refresh(), + result.location.mcp.resource.refresh(), result.location.model.refresh(), result.location.provider.refresh(), result.location.reference.refresh(), diff --git a/packages/tui/src/context/directory.ts b/packages/tui/src/context/directory.ts index b107a40b81..2c566f3373 100644 --- a/packages/tui/src/context/directory.ts +++ b/packages/tui/src/context/directory.ts @@ -1,17 +1,13 @@ import { createMemo } from "solid-js" import { useProject } from "./project" -import { useSync } from "./sync" import { abbreviateHome } from "../runtime" import { useTuiPaths } from "./runtime" export function useDirectory() { const project = useProject() - const sync = useSync() const paths = useTuiPaths() return createMemo(() => { const directory = project.instance.path().directory || paths.cwd - const result = abbreviateHome(directory, paths.home) - if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch - return result + return abbreviateHome(directory, paths.home) }) } diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index d61f5f760b..e8be304866 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -1,7 +1,6 @@ import { createStore } from "solid-js/store" import { createSimpleContext } from "./helper" import { batch, createEffect, createMemo } from "solid-js" -import { useSync } from "./sync" import { useEvent } from "./event" import path from "path" import { useTuiPaths } from "./runtime" @@ -52,7 +51,6 @@ export function recentModels( export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", init: () => { - const sync = useSync() const data = useData() const sdk = useSDK() const toast = useToast() @@ -210,16 +208,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } - if (sync.data.config.model) { - const { providerID, modelID } = parseModel(sync.data.config.model) - if (isModelValid({ providerID, modelID })) { - return { - providerID, - modelID, - } - } - } - for (const item of modelStore.recent) { if (isModelValid(item)) { return item @@ -507,12 +495,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const mcp = { isEnabled(name: string) { - const status = sync.data.mcp[name] - return status?.status === "connected" + return data.location.mcp.server.list()?.find((item) => item.name === name)?.status.status === "connected" }, async toggle(name: string) { - const status = sync.data.mcp[name] - if (status?.status === "connected") { + const status = data.location.mcp.server.list()?.find((item) => item.name === name)?.status.status + if (status === "connected") { // Disable: disconnect the MCP await sdk.client.mcp.disconnect({ name }) } else { diff --git a/packages/tui/src/plugin/adapters.tsx b/packages/tui/src/plugin/adapters.tsx index 7da68b8e86..b97f7b9b73 100644 --- a/packages/tui/src/plugin/adapters.tsx +++ b/packages/tui/src/plugin/adapters.tsx @@ -100,7 +100,7 @@ function mapOptionCb(cb?: (item: TuiDialogSelectOption) => void) { function stateApi(sync: ReturnType, data: ReturnType): TuiPluginApi["state"] { return { get ready() { - return sync.ready + return true }, get config() { return sync.data.config @@ -120,7 +120,7 @@ function stateApi(sync: ReturnType, data: ReturnType, data: ReturnType ({ id: item.id, root: item.root, status: item.status })) }, mcp() { - return Object.entries(sync.data.mcp) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([name, item]) => ({ - name, - status: item.status, - error: item.status === "failed" ? item.error : undefined, - })) + return (data.location.mcp.server.list() ?? []) + .toSorted((a, b) => a.name.localeCompare(b.name)) + .flatMap((item) => + item.status.status === "pending" + ? [] + : [ + { + name: item.name, + status: item.status.status, + error: item.status.status === "failed" ? item.status.error : undefined, + }, + ], + ) }, } } diff --git a/packages/tui/src/routes/home.tsx b/packages/tui/src/routes/home.tsx index 459dbf944e..31570a2550 100644 --- a/packages/tui/src/routes/home.tsx +++ b/packages/tui/src/routes/home.tsx @@ -1,7 +1,6 @@ import { Prompt, type PromptRef } from "../component/prompt" import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js" import { Logo } from "../component/logo" -import { useSync } from "../context/sync" import { Toast } from "../ui/toast" import { useArgs } from "../context/args" import { useRouteData } from "../context/route" @@ -24,7 +23,6 @@ const placeholder = { export function Home() { const pluginRuntime = usePluginRuntime() - const sync = useSync() const route = useRouteData("home") const promptRef = usePromptRef() const [ref, setRef] = createSignal() @@ -61,12 +59,12 @@ export function Home() { once = true } - // Wait for sync and model store to be ready before auto-submitting --prompt + // Wait for the model store to be ready before auto-submitting --prompt. createEffect(() => { const r = ref() if (sent) return if (!r) return - if (!sync.ready || !local.model.ready) return + if (!local.model.ready) return if (!args.prompt) return if (r.current.text !== args.prompt) return sent = true diff --git a/packages/tui/src/routes/home/session-destination.tsx b/packages/tui/src/routes/home/session-destination.tsx index 35611b00b7..bf1e1c1db5 100644 --- a/packages/tui/src/routes/home/session-destination.tsx +++ b/packages/tui/src/routes/home/session-destination.tsx @@ -7,8 +7,8 @@ import { type ParentProps, type Setter, } from "solid-js" -import { useSync } from "../../context/sync" import { useTuiPaths } from "../../context/runtime" +import { useProject } from "../../context/project" export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new"; name: string } @@ -21,11 +21,11 @@ type Context = { const HomeSessionDestinationContext = createContext() export function HomeSessionDestinationProvider(props: ParentProps) { - const sync = useSync() + const project = useProject() const paths = useTuiPaths() const [selected, setDestination] = createSignal() const destination = createMemo( - () => selected() ?? { type: "directory", directory: sync.path.directory || paths.cwd, subdirectory: false }, + () => selected() ?? { type: "directory", directory: project.instance.directory() || paths.cwd, subdirectory: false }, ) return ( (data.location.mcp.list() ?? []).filter((x) => x.status.status === "connected").length) - const mcpError = createMemo(() => (data.location.mcp.list() ?? []).some((x) => x.status.status === "failed")) - const lsp = createMemo(() => Object.keys(sync.data.lsp)) + const mcp = createMemo( + () => (data.location.mcp.server.list() ?? []).filter((x) => x.status.status === "connected").length, + ) + const mcpError = createMemo(() => + (data.location.mcp.server.list() ?? []).some((x) => x.status.status === "failed"), + ) const permissions = createMemo(() => { if (route.data.type !== "session") return [] return data.session.permission.list(route.data.sessionID) ?? [] @@ -68,9 +69,6 @@ export function Footer() { {permissions().length > 1 ? "s" : ""} - - 0 ? theme.success : theme.textMuted }}>• {lsp().length} LSP - diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 7dd3cfb1fc..9e25cc56e9 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -1305,6 +1305,70 @@ test("refreshes integrations after integration updates", async () => { } }) +test("refreshes MCP resources after catalog updates", async () => { + const events = createEventStream() + let requests = 0 + const calls = createFetch((url) => { + if (url.pathname !== "/api/mcp/resource") return + requests++ + return json({ + location: { directory, project: { id: "proj_test", directory } }, + data: { + resources: + requests === 1 + ? [] + : [{ server: "docs", name: "API reference", uri: "https://example.com/api", description: "API docs" }], + templates: [], + }, + }) + }, events) + let data!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + data = useData() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await mounted + await wait(() => data.location.mcp.resource.list() !== undefined) + expect(data.location.mcp.resource.list()).toEqual([]) + + emitEvent(events, { + id: "evt_mcp_resources", + created: 0, + type: "mcp.resources.changed", + data: { server: "docs" }, + }) + await wait(() => data.location.mcp.resource.list()?.length === 1) + expect(data.location.mcp.resource.list()?.[0]).toEqual({ + server: "docs", + name: "API reference", + uri: "https://example.com/api", + description: "API docs", + }) + } finally { + app.renderer.destroy() + } +}) + test("refreshes effective catalog data after catalog updates", async () => { const events = createEventStream() const requests = { model: 0, provider: 0 } diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index 4fe4c8043c..8d0f72206f 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -103,6 +103,11 @@ export function createFetch(override?: FetchHandler, events?: ReturnType