From 36f8cb70548f4db8d506765ba2afc7217d19db19 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 13 Jul 2026 16:12:58 -0400 Subject: [PATCH] refactor(tui): remove legacy sdk client --- bun.lock | 1 - packages/tui/package.json | 3 +- packages/tui/src/app.tsx | 68 ++----- .../tui/src/component/dialog-integration.tsx | 26 +-- .../tui/src/component/dialog-move-session.tsx | 18 +- packages/tui/src/component/dialog-pair.tsx | 6 +- .../tui/src/component/dialog-session-list.tsx | 8 +- .../src/component/dialog-session-rename.tsx | 6 +- packages/tui/src/component/dialog-tag.tsx | 6 +- .../tui/src/component/prompt/autocomplete.tsx | 6 +- packages/tui/src/component/prompt/index.tsx | 26 +-- packages/tui/src/component/prompt/move.tsx | 18 +- .../tui/src/context/{sdk.tsx => client.tsx} | 29 ++- packages/tui/src/context/data.tsx | 56 +++--- packages/tui/src/context/event.ts | 8 +- packages/tui/src/context/local.tsx | 21 +-- packages/tui/src/context/project.tsx | 8 +- packages/tui/src/feature-plugins/builtins.ts | 2 - .../tui/src/feature-plugins/sidebar/files.tsx | 72 -------- .../feature-plugins/system/diff-viewer.tsx | 64 ++----- packages/tui/src/plugin/adapters.tsx | 17 +- .../src/routes/session/composer/shell-tab.tsx | 6 +- .../routes/session/composer/subagents-tab.tsx | 6 +- .../tui/src/routes/session/dialog-fork.tsx | 6 +- .../tui/src/routes/session/dialog-message.tsx | 6 +- packages/tui/src/routes/session/form.tsx | 12 +- packages/tui/src/routes/session/index.tsx | 22 +-- .../tui/src/routes/session/permission.tsx | 12 +- packages/tui/test/app-lifecycle.test.tsx | 2 +- .../test/cli/cmd/tui/notifications.test.ts | 11 +- packages/tui/test/cli/tui/data.test.tsx | 172 +++++++++--------- .../tui/test/cli/tui/diff-viewer.test.tsx | 39 +--- packages/tui/test/cli/tui/form.test.tsx | 8 +- .../test/cli/tui/prompt-submit-race.test.ts | 2 +- packages/tui/test/cli/tui/use-event.test.tsx | 48 +++-- .../fixture/{tui-sdk.ts => tui-client.ts} | 5 - 36 files changed, 316 insertions(+), 510 deletions(-) rename packages/tui/src/context/{sdk.tsx => client.tsx} (86%) delete mode 100644 packages/tui/src/feature-plugins/sidebar/files.tsx rename packages/tui/test/fixture/{tui-sdk.ts => tui-client.ts} (96%) diff --git a/bun.lock b/bun.lock index edb60172f5..7589f01e7f 100644 --- a/bun.lock +++ b/bun.lock @@ -1006,7 +1006,6 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", - "@opencode-ai/sdk": "workspace:*", "@opencode-ai/simulation": "workspace:*", "@opencode-ai/ui": "workspace:*", "@opentui/core": "catalog:", diff --git a/packages/tui/package.json b/packages/tui/package.json index ee70ca50b0..fe3cdeb69c 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -22,7 +22,7 @@ "./context/log": "./src/context/log.tsx", "./context/project": "./src/context/project.tsx", "./context/runtime": "./src/context/runtime.tsx", - "./context/sdk": "./src/context/sdk.tsx", + "./context/client": "./src/context/client.tsx", "./context/theme": "./src/context/theme.tsx", "./context/editor": "./src/context/editor.ts", "./context/clipboard": "./src/context/clipboard.tsx", @@ -55,7 +55,6 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", - "@opencode-ai/sdk": "workspace:*", "@opencode-ai/simulation": "workspace:*", "@opencode-ai/ui": "workspace:*", "@opentui/core": "catalog:", diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 574686ced1..99a58b2e47 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -41,7 +41,7 @@ import { PluginRouteMissing } from "./component/plugin-route-missing" import { ProjectProvider, useProject } from "./context/project" import { EditorContextProvider } from "./context/editor" import { useEvent } from "./context/event" -import { SDKProvider, useSDK } from "./context/sdk" +import { ClientProvider, useClient } from "./context/client" import { StartupLoading } from "./component/startup-loading" import { Reconnecting } from "./component/reconnecting" import { DataProvider, useData } from "./context/data" @@ -55,7 +55,6 @@ import { DialogStatus } from "./component/dialog-status" import { DialogConfig } from "./component/dialog-config" import { DialogDebug } from "./component/dialog-debug" import { DialogPair, type DialogPairCredentials } from "./component/dialog-pair" -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { DialogThemeList } from "./component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { DialogAgent } from "./component/dialog-agent" @@ -66,8 +65,6 @@ import { Session } from "./routes/session" import { PromptHistoryProvider } from "./component/prompt/history" import { FrecencyProvider } from "./component/prompt/frecency" import { PromptStashProvider } from "./component/prompt/stash" -import { DialogAlert } from "./ui/dialog-alert" -import { DialogConfirm } from "./ui/dialog-confirm" import { ToastProvider, useToast } from "./ui/toast" import { isDefaultTitle } from "./util/session" import * as Model from "./util/model" @@ -198,7 +195,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { const endpoint = await reconnectEndpoint(attempt) const next = { baseUrl: endpoint.url, headers: Service.headers(endpoint) } return { - client: createOpencodeClient({ ...next, directory }), api: OpenCode.make(next), } } @@ -336,8 +332,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { } > - - + @@ -426,7 +421,7 @@ function App(props: { const local = useLocal() const keymap = useOpencodeKeymap() const event = useEvent() - const sdk = useSDK() + const client = useClient() const toast = useToast() const themeState = useTheme() const { theme, mode, setMode, locked, lock, unlock } = themeState @@ -475,7 +470,7 @@ function App(props: { route, routes: pluginRuntime.routes, event, - sdk, + client, project, data, theme: themeState, @@ -585,7 +580,7 @@ function App(props: { if (continued || !args.continue) return continued = true const location = data.location.default() - void sdk.api.session + void client.api.session .list({ limit: 1, order: "desc", @@ -600,7 +595,7 @@ function App(props: { route.navigate({ type: "session", sessionID: match }) return } - void sdk.api.session + void client.api.session .fork({ sessionID: match }) .then((result) => route.navigate({ type: "session", sessionID: result.id })) .catch(toast.error) @@ -613,7 +608,7 @@ function App(props: { createEffect(() => { if (forked || !args.sessionID || !args.fork) return forked = true - void sdk.api.session + void client.api.session .fork({ sessionID: args.sessionID }) .then((result) => route.navigate({ type: "session", sessionID: result.id })) .catch(toast.error) @@ -815,7 +810,7 @@ function App(props: { }, category: "System", }, - ...(sdk.reload + ...(client.reload ? [ { name: "server.reload", @@ -826,7 +821,7 @@ function App(props: { toast.show({ variant: "info", message: "Reloading server...", duration: 30000 }) // reload resolves once the replacement service is healthy; the // event stream reattaches through the reconnect loop. - await sdk.reload!() + await client.reload!() .then(() => toast.show({ variant: "success", message: "Server reloaded" })) .catch(toast.error) }, @@ -1092,45 +1087,6 @@ function App(props: { }) }) - event.on("installation.update-available", async (evt) => { - const version = evt.data.version - - const choice = await DialogConfirm.show( - dialog, - `Update Available`, - `A new release v${version} is available. Would you like to update now?`, - "later", - ) - - if (choice !== true) return - - toast.show({ - variant: "info", - message: `Updating to v${version}...`, - duration: 30000, - }) - - const result = await sdk.client.global.upgrade({ target: version }) - - if (result.error || !result.data?.success) { - toast.show({ - variant: "error", - title: "Update Failed", - message: "Update failed", - duration: 10000, - }) - return - } - - await DialogAlert.show( - dialog, - "Update Complete", - `Successfully updated to OpenCode v${result.data.version}. Please restart the application.`, - ) - - void exit() - }) - const plugin = createMemo(() => { if (!ready()) return if (route.data.type !== "plugin") return @@ -1148,7 +1104,7 @@ function App(props: { clearTimeout(reconnectTimer) reconnectTimer = undefined } - const status = sdk.connection.status() + const status = client.connection.status() if (status === "connected") { setShowReconnecting(false) return @@ -1211,7 +1167,7 @@ function App(props: { - + ) diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index 1ae8bbec77..74aa76040b 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -8,7 +8,7 @@ import type { import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" import { useClipboard } from "../context/clipboard" import { useData } from "../context/data" -import { useSDK } from "../context/sdk" +import { useClient } from "../context/client" import { useTheme } from "../context/theme" import { useBindings } from "../keymap" import { useDialog } from "../ui/dialog" @@ -102,7 +102,7 @@ function manageConnections( ) { dialog.replace(() => { const data = useData() - const sdk = useSDK() + const client = useClient() const toast = useToast() return ( { - void sdk.api.credential + void client.api.credential .remove({ credentialID: connection.id, location: location(data) }) .then(() => disconnected(integration.name, data, dialog, toast)) .catch(toast.error) @@ -172,7 +172,7 @@ function KeyMethod(props: { }) { const data = useData() const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const toast = useToast() const { theme } = useTheme() const [error, setError] = createSignal() @@ -183,7 +183,7 @@ function KeyMethod(props: { placeholder="API key" onConfirm={(key) => { if (!key) return - void sdk.api.integration + void client.api.integration .connect.key({ integrationID: props.integration.id, location: location(data), @@ -218,11 +218,11 @@ function OAuthStarting(props: { }) { const data = useData() const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const toast = useToast() onMount(() => { - void sdk.api.integration + void client.api.integration .connect.oauth({ integrationID: props.integration.id, location: location(data), @@ -267,7 +267,7 @@ function OAuthAuto(props: { }) { const data = useData() const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const toast = useToast() const clipboard = useClipboard() let timer: ReturnType | undefined @@ -291,7 +291,7 @@ function OAuthAuto(props: { })) const poll = () => { - void sdk.api.integration + void client.api.integration .attempt.status({ attemptID: props.attempt.attemptID, location: location(data) }) .then((result) => { const status = result.data @@ -318,7 +318,7 @@ function OAuthAuto(props: { onCleanup(() => { if (timer) clearTimeout(timer) if (settled) return - void sdk.api.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) + void client.api.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) }) return ( @@ -340,7 +340,7 @@ function OAuthCode(props: { }) { const data = useData() const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const toast = useToast() const { theme } = useTheme() const [error, setError] = createSignal() @@ -348,7 +348,7 @@ function OAuthCode(props: { onCleanup(() => { if (settled) return - void sdk.api.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) + void client.api.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) }) return ( @@ -357,7 +357,7 @@ function OAuthCode(props: { placeholder="Authorization code" onConfirm={(code) => { if (!code) return - void sdk.api.integration + void client.api.integration .attempt.complete({ attemptID: props.attempt.attemptID, location: location(data), code }) .then(() => { settled = true diff --git a/packages/tui/src/component/dialog-move-session.tsx b/packages/tui/src/component/dialog-move-session.tsx index 3ea26a50d5..bc30045b40 100644 --- a/packages/tui/src/component/dialog-move-session.tsx +++ b/packages/tui/src/component/dialog-move-session.tsx @@ -4,7 +4,7 @@ import { createMemo, createResource, createSignal, onMount, Show } from "solid-j import path from "path" import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" -import { useSDK } from "../context/sdk" +import { useClient } from "../context/client" import { useTheme } from "../context/theme" import { useData } from "../context/data" import { abbreviateHome } from "../runtime" @@ -35,7 +35,7 @@ type DialogMoveSessionProps = { export function DialogMoveSession(props: DialogMoveSessionProps) { const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const dimensions = useTerminalDimensions() const { theme } = useTheme() const sessionData = useData() @@ -63,7 +63,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { const [loadedProject] = createResource( () => (projectContext.project() === undefined ? props.projectID : undefined), (projectID) => - sdk.api.project + client.api.project .current({ location: { directory: projectContext.instance.directory() || paths.cwd } }) .then((project) => (project.id === projectID ? project.directory : undefined)) .catch(() => undefined), @@ -78,11 +78,11 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { async (projectID, info): Promise | undefined> => { try { const location = { directory: projectContext.instance.directory() || paths.cwd } - await sdk.api.projectCopy.refresh({ + await client.api.projectCopy.refresh({ projectID, location, }) - const directories = await sdk.api.project.directories({ + const directories = await client.api.project.directories({ projectID, location, }) @@ -232,7 +232,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { setToDelete(undefined) setRemoving(selected.directory) setWorking(true) - const error = await sdk.api.projectCopy + const error = await client.api.projectCopy .remove({ projectID: props.projectID, location: { directory: projectContext.instance.directory() || paths.cwd }, @@ -247,7 +247,9 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { setRemoving(undefined) setWorking(false) if (isRecord(error) && isRecord(error.data) && error.data.forceRequired === true) { - const status = await sdk.client.vcs.status({ directory: selected.directory }).catch(() => undefined) + const status = await client.api.vcs + .status({ location: { directory: selected.directory } }) + .catch(() => undefined) const choice = await DialogWorkspaceFileChanges.show(dialog, status?.data ?? [], { title: "Delete working copy?", message: "This working copy has file changes. Do you want to delete it anyway?", @@ -257,7 +259,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { return } reopen(selected.directory) - const forcedError = await sdk.api.projectCopy + const forcedError = await client.api.projectCopy .remove({ projectID: props.projectID, location: { directory: projectContext.instance.directory() || paths.cwd }, diff --git a/packages/tui/src/component/dialog-pair.tsx b/packages/tui/src/component/dialog-pair.tsx index 807cac0abb..d1b5b10787 100644 --- a/packages/tui/src/component/dialog-pair.tsx +++ b/packages/tui/src/component/dialog-pair.tsx @@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core" import { useTerminalDimensions } from "@opentui/solid" import { createMemo, createResource, createSignal, For, Show } from "solid-js" import { renderUnicodeCompact } from "uqr" -import { useSDK } from "../context/sdk" +import { useClient } from "../context/client" import { useTheme } from "../context/theme" import { useDialog } from "../ui/dialog" import { errorMessage } from "../util/error" @@ -13,7 +13,7 @@ export type DialogPairCredentials = { } export function DialogPair(props: { credentials?: DialogPairCredentials }) { - const sdk = useSDK() + const client = useClient() const dialog = useDialog() const dimensions = useTerminalDimensions() const { theme } = useTheme() @@ -25,7 +25,7 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) { dialog.setCentered(true) const [server] = createResource(() => - sdk.api.server + client.api.server .get() .catch((error) => { setLoadError(error) diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 2d305f54e6..07830e4202 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -8,7 +8,7 @@ import { useData } from "../context/data" import { Locale } from "../util/locale" import { useProject } from "../context/project" import { useTheme } from "../context/theme" -import { useSDK } from "../context/sdk" +import { useClient } from "../context/client" import { useLocal } from "../context/local" import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" @@ -23,7 +23,7 @@ export function DialogSessionList() { const data = useData() const project = useProject() const { theme } = useTheme() - const sdk = useSDK() + const client = useClient() const local = useLocal() const toast = useToast() const [search, setSearch] = createDebouncedSignal("", 150) @@ -36,7 +36,7 @@ export function DialogSessionList() { if (!query) return const location = data.location.default() try { - const response = await sdk.api.session.list({ + const response = await client.api.session.list({ search: query, limit: 50, order: "desc", @@ -140,7 +140,7 @@ export function DialogSessionList() { setToDelete(option.value) return } - void sdk.api.session.remove({ sessionID: option.value }).catch((error) => { + void client.api.session.remove({ sessionID: option.value }).catch((error) => { setToDelete(undefined) toast.show({ message: `Failed to delete session: ${errorMessage(error)}`, diff --git a/packages/tui/src/component/dialog-session-rename.tsx b/packages/tui/src/component/dialog-session-rename.tsx index f2b1d07832..034bc4bcbb 100644 --- a/packages/tui/src/component/dialog-session-rename.tsx +++ b/packages/tui/src/component/dialog-session-rename.tsx @@ -1,12 +1,12 @@ import { DialogPrompt } from "../ui/dialog-prompt" import { type DialogContext, useDialog } from "../ui/dialog" -import { useSDK } from "../context/sdk" +import { useClient } from "../context/client" import { useToast } from "../ui/toast" import { errorMessage } from "../util/error" export function DialogSessionRename(props: { sessionID: string; currentTitle?: string }) { const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const toast = useToast() return ( @@ -17,7 +17,7 @@ export function DialogSessionRename(props: { sessionID: string; currentTitle?: s onConfirm={(value) => { const title = value.trim() if (!title) return - void sdk.api.session + void client.api.session .rename({ sessionID: props.sessionID, title }) .then(() => dialog.clear()) .catch((error) => diff --git a/packages/tui/src/component/dialog-tag.tsx b/packages/tui/src/component/dialog-tag.tsx index aa32c98ab7..69d0c51de1 100644 --- a/packages/tui/src/component/dialog-tag.tsx +++ b/packages/tui/src/component/dialog-tag.tsx @@ -2,11 +2,11 @@ import { createMemo, createResource } from "solid-js" import { DialogSelect } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { useProject } from "../context/project" -import { useSDK } from "../context/sdk" +import { useClient } from "../context/client" import { createStore } from "solid-js/store" export function DialogTag(props: { onSelect?: (value: string) => void }) { - const sdk = useSDK() + const client = useClient() const dialog = useDialog() const project = useProject() @@ -17,7 +17,7 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) { const [files] = createResource( () => [store.filter], async () => { - const result = await sdk.api.file + const result = await client.api.file .find({ query: store.filter, type: "file", diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 3f01827880..03e1b5fcdb 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -7,7 +7,7 @@ import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Sh import { createStore } from "solid-js/store" import { useEditorContext } from "../../context/editor" import { useProject } from "../../context/project" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { useData } from "../../context/data" import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" @@ -84,7 +84,7 @@ export function Autocomplete(props: { promptPartTypeId: () => number }) { const editor = useEditorContext() - const sdk = useSDK() + const client = useClient() const data = useData() const project = useProject() const slashes = useCommandSlashes() @@ -315,7 +315,7 @@ export function Autocomplete(props: { if (referenceMatch()) return [] const { lineRange, baseQuery } = extractLineRange(input.query ?? "") - const result = await sdk.api.file + const result = await client.api.file .find({ query: baseQuery, limit: 20, diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 55a7f2ee7d..94ecbb4654 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -21,7 +21,7 @@ import { EmptyBorder, SplitBorder } from "../../ui/border" import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime" import { useClipboard } from "../../context/clipboard" import { Spinner } from "../spinner" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { useRoute } from "../../context/route" import { useProject } from "../../context/project" import { useEvent } from "../../context/event" @@ -148,7 +148,7 @@ export function Prompt(props: PromptProps) { const paths = useTuiPaths() const terminalEnvironment = useTuiTerminalEnvironment() const clipboard = useClipboard() - const sdk = useSDK() + const client = useClient() const editor = useEditorContext() const route = useRoute() const project = useProject() @@ -421,7 +421,7 @@ export function Prompt(props: PromptProps) { }, 5000) if (store.interrupt >= 2) { - void sdk.api.session.interrupt({ + void client.api.session.interrupt({ sessionID: props.sessionID, }) setStore("interrupt", 0) @@ -440,7 +440,7 @@ export function Prompt(props: PromptProps) { if (!input.focused) return if (!props.sessionID) return - void sdk.api.session.background({ + void client.api.session.background({ sessionID: props.sessionID, }) dialog.clear() @@ -965,7 +965,7 @@ export function Prompt(props: PromptProps) { finishMoveProgress = Boolean(move.progress()) const location = data.location.default() - const created = await sdk.api.session + const created = await client.api.session .create({ location: directory ? { directory } : location, agent: agent.id, @@ -1009,7 +1009,7 @@ export function Prompt(props: PromptProps) { if (store.mode === "shell") { move.startSubmit() - void sdk.api.session.shell({ + void client.api.session.shell({ sessionID, command: inputText, }) @@ -1028,7 +1028,7 @@ export function Prompt(props: PromptProps) { const restOfInput = firstLineEnd === -1 ? "" : inputText.slice(firstLineEnd + 1) const args = firstLineArgs.join(" ") + (restOfInput ? "\n" + restOfInput : "") - void sdk.api.session + void client.api.session .command({ sessionID, command: command.slice(1), @@ -1048,7 +1048,7 @@ export function Prompt(props: PromptProps) { ) ) { move.startSubmit() - void sdk.api.session.skill({ + void client.api.session.skill({ sessionID, skill: inputText.split("\n")[0].split(" ")[0].slice(1), }) @@ -1059,20 +1059,20 @@ export function Prompt(props: PromptProps) { session = data.session.get(sessionID) } if (session?.agent !== agent.id) { - await sdk.api.session.switchAgent({ sessionID, agent: agent.id }) + await client.api.session.switchAgent({ sessionID, agent: agent.id }) } if ( session?.model?.providerID !== selectedModel.providerID || session.model.id !== selectedModel.modelID || session.model.variant !== variant ) { - await sdk.api.session.switchModel({ + await client.api.session.switchModel({ sessionID, model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant }, }) } if (session?.revert) { - const error = await sdk.api.session.revert.commit({ sessionID }).then( + const error = await client.api.session.revert.commit({ sessionID }).then( () => undefined, (error) => error, ) @@ -1083,7 +1083,7 @@ export function Prompt(props: PromptProps) { } if (pendingEditorSelection) { // Keep editor context hidden while admitting it before the corresponding user prompt. - const error = await sdk.api.session + const error = await client.api.session .synthetic({ sessionID, text: formatEditorContext(pendingEditorSelection), @@ -1098,7 +1098,7 @@ export function Prompt(props: PromptProps) { return false } } - const error = await sdk.api.session + const error = await client.api.session .prompt({ sessionID, text: inputText, diff --git a/packages/tui/src/component/prompt/move.tsx b/packages/tui/src/component/prompt/move.tsx index 1139385034..9421916374 100644 --- a/packages/tui/src/component/prompt/move.tsx +++ b/packages/tui/src/component/prompt/move.tsx @@ -3,7 +3,7 @@ import path from "path" import { useTuiPaths } from "../../context/runtime" import { errorMessage } from "../../util/error" import { useDialog } from "../../ui/dialog" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { useToast } from "../../ui/toast" import { DialogMoveSession, type MoveSessionSelection } from "../dialog-move-session" import { DialogWorkspaceFileChanges } from "../dialog-workspace-file-changes" @@ -17,7 +17,7 @@ function moveReminderText(directory: string) { export function usePromptMove(input: { projectID: () => string | undefined; sessionID: () => string | undefined }) { const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const toast = useToast() const homeDestination = useHomeSessionDestination() const project = useProject() @@ -33,7 +33,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess setCreating(true) setProgress("Creating copy") try { - const result = await sdk.api.projectCopy.create({ + const result = await client.api.projectCopy.create({ projectID, location: { directory: project.instance.directory() || paths.cwd }, strategy: "git_worktree", @@ -44,7 +44,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess if (!directory) throw new Error("No project copy directory returned") // Call a location-based route to make sure it's bootstrapped before moving on. - await sdk.api.location.get({ location: { directory } }) + await client.api.location.get({ location: { directory } }) setProgress("Creating session") return directory @@ -98,7 +98,9 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess async function moveExistingSession(sessionID: string, selection: MoveSessionSelection) { const session = await resolveSession(sessionID) - const status = await sdk.client.vcs.status({ directory: session?.location.directory }).catch(() => undefined) + const status = await client.api.vcs + .status({ location: session?.location.directory ? { directory: session.location.directory } : undefined }) + .catch(() => undefined) const choice = status?.data?.length ? await DialogWorkspaceFileChanges.show(dialog, status.data) : "no" if (!choice) return dialog.clear() @@ -110,8 +112,8 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess } setProgress("Moving session") try { - await sdk.api.session.move({ sessionID, destination: { directory }, moveChanges: choice === "yes" }) - await sdk.api.session + await client.api.session.move({ sessionID, destination: { directory }, moveChanges: choice === "yes" }) + await client.api.session .synthetic({ sessionID, text: moveReminderText(directory), resume: false }) .catch(() => undefined) dialog.clear() @@ -129,7 +131,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess if (projectID) return projectID const sessionID = input.sessionID() if (sessionID) return (await resolveSession(sessionID))?.projectID - return sdk.api.project + return client.api.project .current({ location: { directory: project.instance.directory() || paths.cwd } }) .then((project) => project.id) .catch(() => undefined) diff --git a/packages/tui/src/context/sdk.tsx b/packages/tui/src/context/client.tsx similarity index 86% rename from packages/tui/src/context/sdk.tsx rename to packages/tui/src/context/client.tsx index d1364378ef..04e347900a 100644 --- a/packages/tui/src/context/sdk.tsx +++ b/packages/tui/src/context/client.tsx @@ -1,13 +1,12 @@ import type { OpenCodeClient, OpenCodeEvent } from "@opencode-ai/client" -import type { OpencodeClient } from "@opencode-ai/sdk/v2" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "./helper" import { useLog } from "./log" -export type SDKConnectionStatus = "connected" | "connecting" | "reconnecting" -export type SDKConnectionEvent = { +export type ClientConnectionStatus = "connected" | "connecting" | "reconnecting" +export type ClientConnectionEvent = { readonly type: "client.connection" readonly created: number readonly data: { @@ -17,27 +16,25 @@ export type SDKConnectionEvent = { } } -type SDKEventMap = { [Type in OpenCodeEvent["type"]]: Extract } +type ClientEventMap = { [Type in OpenCodeEvent["type"]]: Extract } const connectTimeout = 2_000 const connectionHistoryLimit = 50 -export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ - name: "SDK", +export const { use: useClient, provider: ClientProvider } = createSimpleContext({ + name: "Client", init: (props: { - client: OpencodeClient api: OpenCodeClient - reconnect?: (attempt: number) => Promise<{ client: OpencodeClient; api: OpenCodeClient }> + reconnect?: (attempt: number) => Promise<{ api: OpenCodeClient }> // Stops and starts the managed service; present only in service mode. reload?: () => Promise }) => { - const log = useLog({ component: "sdk" }) + const log = useLog({ component: "client" }) const abort = new AbortController() - const history: SDKConnectionEvent[] = [] - let client = props.client + const history: ClientConnectionEvent[] = [] let api = props.api - const events = createGlobalEmitter() + const events = createGlobalEmitter() const [connection, setConnection] = createStore<{ - status: SDKConnectionStatus + status: ClientConnectionStatus attempt: number error?: string }>({ @@ -46,7 +43,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ }) let stream: AbortController | undefined - function record(status: SDKConnectionEvent["data"]["status"], attempt: number, error?: string) { + function record(status: ClientConnectionEvent["data"]["status"], attempt: number, error?: string) { history.push({ type: "client.connection", created: Date.now(), data: { status, attempt, error } }) if (history.length > connectionHistoryLimit) history.shift() } @@ -121,7 +118,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ const next = await props.reconnect(attempt).catch(() => undefined) if (abort.signal.aborted || controller.signal.aborted) return if (next) { - client = next.client api = next.api } } @@ -144,9 +140,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ }) return { - get client() { - return client - }, get api() { return api }, diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index c5f4fddd85..66ca56fcc9 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -30,7 +30,7 @@ import type { import type { Data } from "@opencode-ai/plugin/v2/tui/context" import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" -import { useSDK } from "./sdk" +import { useClient } from "./client" import { createSignal, onCleanup } from "solid-js" export type DataSessionStatus = "idle" | "running" @@ -110,7 +110,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ location: {}, }) - const sdk = useSDK() + const client = useClient() const [defaultLocation, setDefaultLocation] = createSignal({ directory: process.cwd(), }) @@ -328,7 +328,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ time: { created: event.created }, }) }) - void sdk.api.session + void client.api.session .message({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) }) .then((item) => { message.update(event.data.sessionID, (draft, index) => { @@ -851,8 +851,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ } const result = { - on: sdk.event.on, - listen: sdk.event.listen, + on: client.event.on, + listen: client.event.listen, session: { list() { return Object.values(store.session.info).toSorted((a, b) => b.time.updated - a.time.updated) @@ -899,7 +899,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.session.pending[sessionID] ?? [] }, async refresh(sessionID: string) { - const pending = await sdk.api.session.pending.list({ sessionID }) + const pending = await client.api.session.pending.list({ sessionID }) setStore("session", "pending", sessionID, reconcile(pending)) setStore( "session", @@ -916,7 +916,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }, }, async refresh(sessionID: string) { - setStore("session", "info", sessionID, await sdk.api.session.get({ sessionID })) + setStore("session", "info", sessionID, await client.api.session.get({ sessionID })) registerSession(sessionID) }, message: { @@ -929,7 +929,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return position === undefined ? undefined : messages?.[position] }, async refresh(sessionID: string) { - const messages = (await sdk.api.message.list({ sessionID, limit: 200, order: "desc" })).data.toReversed() + const messages = (await client.api.message.list({ sessionID, limit: 200, order: "desc" })).data.toReversed() messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index]))) setStore("session", "message", sessionID, reconcile(messages)) }, @@ -939,7 +939,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.session.permission[sessionID] }, async refresh(sessionID: string) { - setStore("session", "permission", sessionID, await sdk.api.permission.list({ sessionID })) + setStore("session", "permission", sessionID, await client.api.permission.list({ sessionID })) }, }, form: { @@ -952,7 +952,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }, async refresh(sessionID: string, ref?: LocationRef) { if (sessionID === "global") { - const response = await sdk.api.form.request.list({ location: locationQuery(ref ?? defaultLocation()) }) + const response = await client.api.form.request.list({ location: locationQuery(ref ?? defaultLocation()) }) const location = { directory: response.location.directory, workspaceID: response.location.workspaceID, @@ -966,7 +966,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ]) return } - setStore("session", "form", sessionID, await sdk.api.form.list({ sessionID })) + setStore("session", "form", sessionID, await client.api.form.list({ sessionID })) }, }, }, @@ -976,7 +976,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.project.permission[projectID] }, async refresh(projectID: string) { - setStore("project", "permission", projectID, await sdk.api.permission.saved.list({ projectID })) + setStore("project", "permission", projectID, await client.api.permission.saved.list({ projectID })) }, }, }, @@ -990,7 +990,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ .find((shell) => shell !== undefined) }, async refresh(ref?: LocationRef) { - const result = await sdk.api.shell.list({ location: locationQuery(ref) }) + const result = await client.api.shell.list({ location: locationQuery(ref) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], @@ -1003,7 +1003,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return defaultLocation() }, async refresh(ref?: LocationRef) { - const location = await sdk.api.location.get({ location: locationQuery(ref ?? defaultLocation()) }) + const location = await client.api.location.get({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(location) if (!store.location[key]) setStore("location", key, {}) if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID }) @@ -1013,7 +1013,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.agent }, async refresh(ref?: LocationRef) { - const result = await sdk.api.agent.list({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await client.api.agent.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], agent: result.data }) }, @@ -1023,7 +1023,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.command }, async refresh(ref?: LocationRef) { - const result = await sdk.api.command.list({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await client.api.command.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], command: result.data }) }, @@ -1033,7 +1033,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.integration }, async refresh(ref?: LocationRef) { - const result = await sdk.api.integration.list({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await client.api.integration.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], integration: result.data }) }, @@ -1044,7 +1044,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.mcp?.server }, async refresh(ref?: LocationRef) { - const result = await sdk.api.mcp.list({ location: locationQuery(ref) }) + const result = await client.api.mcp.list({ location: locationQuery(ref) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], @@ -1057,7 +1057,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.mcp?.resource }, async refresh(ref?: LocationRef) { - const result = await sdk.api.mcp.resource.catalog({ location: locationQuery(ref) }) + const result = await client.api.mcp.resource.catalog({ location: locationQuery(ref) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], @@ -1071,7 +1071,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.model }, async refresh(ref?: LocationRef) { - const result = await sdk.api.model.list({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await client.api.model.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], model: result.data }) }, @@ -1081,7 +1081,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.provider }, async refresh(ref?: LocationRef) { - const result = await sdk.api.provider.list({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await client.api.provider.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], provider: result.data }) }, @@ -1091,7 +1091,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.reference }, async refresh(ref?: LocationRef) { - const result = await sdk.api.reference.list({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await client.api.reference.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], reference: result.data }) }, @@ -1101,7 +1101,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.location[locationKey(location ?? defaultLocation())]?.skill }, async refresh(ref?: LocationRef) { - const result = await sdk.api.skill.list({ location: locationQuery(ref ?? defaultLocation()) }) + const result = await client.api.skill.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(result.location) setStore("location", key, { ...store.location[key], skill: result.data }) }, @@ -1113,7 +1113,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ async function bootstrap() { if (bootstrapping) return bootstrapping bootstrapping = Promise.allSettled([ - sdk.api.session + client.api.session .list({ limit: 50, order: "desc", @@ -1130,7 +1130,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ) for (const session of response.data) registerSession(session.id) }), - sdk.api.permission.request.list({ location: locationQuery(defaultLocation()) }).then((response) => { + client.api.permission.request.list({ location: locationQuery(defaultLocation()) }).then((response) => { const permissions = response.data.reduce>( (result, request) => ({ ...result, @@ -1140,7 +1140,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ) setStore("session", "permission", reconcile(permissions)) }), - sdk.api.form.request.list({ location: locationQuery(defaultLocation()) }).then((response) => { + client.api.form.request.list({ location: locationQuery(defaultLocation()) }).then((response) => { const location = { directory: response.location.directory, workspaceID: response.location.workspaceID, @@ -1193,7 +1193,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ } function refreshActive() { - void sdk.api.session + void client.api.session .active() .then((active) => { setStore( @@ -1206,7 +1206,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ } onCleanup( - sdk.event.listen(({ details }) => { + client.event.listen(({ details }) => { if (details.type === "server.connected") { const messages = connected ? Object.keys(store.session.message) : [] const compactions = connected ? Object.keys(store.session.compaction) : [] diff --git a/packages/tui/src/context/event.ts b/packages/tui/src/context/event.ts index 9accb5ec5e..eb8c5b0315 100644 --- a/packages/tui/src/context/event.ts +++ b/packages/tui/src/context/event.ts @@ -1,5 +1,5 @@ import type { OpenCodeEvent } from "@opencode-ai/client" -import { useSDK } from "./sdk" +import { useClient } from "./client" type EventMetadata = { directory: string | undefined @@ -7,10 +7,10 @@ type EventMetadata = { } export function useEvent() { - const sdk = useSDK() + const client = useClient() function subscribe(handler: (event: OpenCodeEvent, metadata: EventMetadata) => void) { - return sdk.event.listen(({ details }) => { + return client.event.listen(({ details }) => { if (details.type === "server.connected") return handler(details, { directory: details.location?.directory, workspace: details.location?.workspaceID }) }) @@ -20,7 +20,7 @@ export function useEvent() { type: T, handler: (event: Extract, metadata: EventMetadata) => void, ) { - return sdk.event.on(type, (event) => { + return client.event.on(type, (event) => { handler(event, { directory: event.location?.directory, workspace: event.location?.workspaceID }) }) } diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index e8be304866..56655646ec 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -5,7 +5,7 @@ import { useEvent } from "./event" import path from "path" import { useTuiPaths } from "./runtime" import { useArgs } from "./args" -import { useSDK } from "./sdk" +import { useClient } from "./client" import { RGBA } from "@opentui/core" import { readJson, writeJsonAtomic } from "../util/persistence" import { useTheme } from "./theme" @@ -52,7 +52,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", init: () => { const data = useData() - const sdk = useSDK() + const client = useClient() const toast = useToast() const theme = useTheme().theme const route = useRoute() @@ -493,22 +493,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const session = createSession() - const mcp = { - isEnabled(name: string) { - return data.location.mcp.server.list()?.find((item) => item.name === name)?.status.status === "connected" - }, - async toggle(name: string) { - 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 { - // Enable/Retry: connect the MCP (handles disabled, failed, and other states) - await sdk.client.mcp.connect({ name }) - } - }, - } - createEffect(() => { const value = agent.current() if (!value?.model) return @@ -523,7 +507,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const result = { model, agent, - mcp, session, permission, } diff --git a/packages/tui/src/context/project.tsx b/packages/tui/src/context/project.tsx index 73584ceda1..466e663ab1 100644 --- a/packages/tui/src/context/project.tsx +++ b/packages/tui/src/context/project.tsx @@ -1,12 +1,12 @@ import { batch } from "solid-js" import { createStore, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" -import { useSDK } from "./sdk" +import { useClient } from "./client" export const { use: useProject, provider: ProjectProvider } = createSimpleContext({ name: "Project", init: () => { - const sdk = useSDK() + const client = useClient() const defaultPath = { home: "", @@ -33,8 +33,8 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex async function sync() { const workspace = store.workspace.current const location = { workspace } - const current = await sdk.api.location.get({ location }) - const directories = await sdk.api.project.directories({ projectID: current.project.id, location }) + const current = await client.api.location.get({ location }) + const directories = await client.api.project.directories({ projectID: current.project.id, location }) batch(() => { setStore( "instance", diff --git a/packages/tui/src/feature-plugins/builtins.ts b/packages/tui/src/feature-plugins/builtins.ts index 0069d56937..9684b71334 100644 --- a/packages/tui/src/feature-plugins/builtins.ts +++ b/packages/tui/src/feature-plugins/builtins.ts @@ -3,7 +3,6 @@ import type { PluginRuntime } from "../plugin/runtime" import HomeFooter from "./home/footer" import HomeTips from "./home/tips" import SidebarContext from "./sidebar/context" -import SidebarFiles from "./sidebar/files" import SidebarFooter from "./sidebar/footer" import SidebarLsp from "./sidebar/lsp" import SidebarMcp from "./sidebar/mcp" @@ -26,7 +25,6 @@ export function createBuiltinPlugins(): BuiltinTuiPlugin[] { SidebarContext, SidebarMcp, SidebarLsp, - SidebarFiles, SidebarFooter, Notifications, PluginManager, diff --git a/packages/tui/src/feature-plugins/sidebar/files.tsx b/packages/tui/src/feature-plugins/sidebar/files.tsx deleted file mode 100644 index e76db91444..0000000000 --- a/packages/tui/src/feature-plugins/sidebar/files.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" -import type { BuiltinTuiPlugin } from "../builtins" -import { createMemo, For, Show, createSignal } from "solid-js" -import { FilePath } from "../../ui/file-path" - -const id = "internal:sidebar-files" - -function changeCountWidth(item: { additions: number; deletions: number }) { - return [item.additions ? `+${item.additions}` : "", item.deletions ? `-${item.deletions}` : ""] - .filter(Boolean) - .join(" ").length -} - -function View(props: { api: TuiPluginApi; session_id: string }) { - const [open, setOpen] = createSignal(true) - const theme = () => props.api.theme.current - const list = createMemo(() => props.api.state.session.diff(props.session_id)) - - return ( - 0}> - - list().length > 2 && setOpen((x) => !x)}> - 2}> - {open() ? "▼" : "▶"} - - - Modified Files - - - - - {(item) => ( - - - - - +{item.additions} - - - -{item.deletions} - - - - )} - - - - - ) -} - -const tui: TuiPlugin = async (api) => { - api.slots.register({ - order: 500, - slots: { - sidebar_content(_ctx, props) { - return - }, - }, - }) -} - -const plugin: BuiltinTuiPlugin = { - id, - tui, -} - -export default plugin diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index 6444a86950..650bd29474 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @opentui/solid */ import type { TuiPlugin, TuiPluginApi, TuiRouteCurrent } from "@opencode-ai/plugin/tui" -import type { FileDiffInfo, FileDiffLegacyInfo } from "@opencode-ai/client" +import type { FileDiffInfo } from "@opencode-ai/client" import { TextAttributes, type BorderSides, @@ -11,7 +11,7 @@ import { import { LANGUAGE_EXTENSIONS } from "../../util/filetype" import { useBindings, useCommandShortcut } from "../../keymap" import { useTheme } from "../../context/theme" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { useTerminalDimensions } from "@opentui/solid" import path from "path" import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js" @@ -42,7 +42,7 @@ const MIN_SPLIT_WIDTH = 100 const FILE_TREE_WIDTH = 32 const PLAIN_TEXT_FILETYPE = "opencode-plain-text" const VCS_DIFF_CONTEXT_LINES = 12 -type DiffMode = "working" | "branch" | "last-turn" +type DiffMode = "working" | "branch" type DiffViewerFocus = "patches" | "files" type DiffView = "split" | "unified" type SelectedHunk = { readonly fileIndex: number; readonly hunkIndex: number; readonly scrollTop: number } @@ -55,20 +55,14 @@ type DiffFile = { readonly status: "added" | "deleted" | "modified" } -const normalizeDiffs = (diffs: readonly (FileDiffInfo | FileDiffLegacyInfo)[]): DiffFile[] => - diffs.flatMap((item) => - item.file - ? [ - { - file: item.file, - patch: item.patch, - additions: item.additions, - deletions: item.deletions, - status: item.status ?? "modified", - } satisfies DiffFile, - ] - : [], - ) +const normalizeDiffs = (diffs: readonly FileDiffInfo[]): DiffFile[] => + diffs.map((item) => ({ + file: item.file, + patch: item.patch, + additions: item.additions, + deletions: item.deletions, + status: item.status, + })) function filetype(input?: string) { if (!input) return "none" @@ -82,14 +76,13 @@ function storedView(value: unknown): DiffView | undefined { } function diffSourceLabel(mode: DiffMode) { - if (mode === "last-turn") return "last turn" if (mode === "branch") return "main branch" return "working tree" } function DiffViewer(props: { api: TuiPluginApi }) { const dimensions = useTerminalDimensions() - const sdk = useSDK() + const client = useClient() const config = useConfig() const themeState = useTheme() const theme = () => props.api.theme.current @@ -98,7 +91,6 @@ function DiffViewer(props: { api: TuiPluginApi }) { | { mode?: DiffMode sessionID?: string - messageID?: string returnRoute?: TuiRouteCurrent } | undefined @@ -108,22 +100,11 @@ function DiffViewer(props: { api: TuiPluginApi }) { return { mode: mode(), sessionID, - messageID: params()?.messageID, directory: sessionID ? props.api.state.session.get(sessionID)?.directory : undefined, } }) const [diff] = createResource(diffInput, async (input) => { - if (input.mode === "last-turn") { - const sessionID = input.sessionID - if (!sessionID) return [] - const result = await props.api.client.session.diff( - { sessionID, messageID: input.messageID }, - { throwOnError: true }, - ) - return normalizeDiffs(result.data ?? []) - } - - const result = await sdk.api.vcs.diff( + const result = await client.api.vcs.diff( { location: input.directory ? { directory: input.directory } : undefined, mode: input.mode, @@ -704,26 +685,16 @@ function DiffViewer(props: { api: TuiPluginApi }) { ] const switchDiffOptions = createMemo(() => { - const vcs = props.api.state.vcs return [ { title: "Working tree", value: "working" as const, description: "Show current git changes", }, - ...(vcs?.branch && vcs.default_branch && vcs.branch !== vcs.default_branch - ? [ - { - title: "Main branch", - value: "branch" as const, - description: "Show changes compared to main branch", - }, - ] - : []), { - title: "Last turn", - value: "last-turn" as const, - description: "Show changes from the last assistant turn", + title: "Main branch", + value: "branch" as const, + description: "Show changes compared to main branch", }, ] }) @@ -742,7 +713,6 @@ function DiffViewer(props: { api: TuiPluginApi }) { props.api.route.navigate(ROUTE, { mode: option.value, sessionID: params()?.sessionID, - messageID: params()?.messageID, returnRoute: params()?.returnRoute, }) }, @@ -1011,7 +981,7 @@ function DiffViewerHelpDialog() { { shortcut: useCommandShortcut("diff.switch_source"), action: "Switch source", - description: "Choose working tree, main branch, or last-turn changes", + description: "Choose working tree or main branch changes", }, { shortcut: useCommandShortcut("diff.toggle_view"), diff --git a/packages/tui/src/plugin/adapters.tsx b/packages/tui/src/plugin/adapters.tsx index 4c27c7f9b4..b3c9d81ab7 100644 --- a/packages/tui/src/plugin/adapters.tsx +++ b/packages/tui/src/plugin/adapters.tsx @@ -2,7 +2,7 @@ import type { TuiDialogSelectOption, TuiPluginApi, TuiSlotProps } from "@opencod import type { Config } from "../config" import type { useEvent } from "../context/event" import type { useRoute } from "../context/route" -import type { useSDK } from "../context/sdk" +import type { useClient } from "../context/client" import type { useData } from "../context/data" import type { useProject } from "../context/project" import type { useTheme } from "../context/theme" @@ -28,7 +28,7 @@ type Input = { route: ReturnType routes: PluginRoutes event: ReturnType - sdk: ReturnType + client: ReturnType project: ReturnType data: ReturnType theme: ReturnType @@ -167,6 +167,15 @@ function appApi(version: string): TuiPluginApi["app"] { } } +const unsupportedClient = new Proxy( + {}, + { + get() { + throw new Error("The legacy plugin client is not supported in V2") + }, + }, +) as TuiPluginApi["client"] + export function createTuiApiAdapters(input: Input): Omit { return { app: appApi(input.version), @@ -292,9 +301,7 @@ export function createTuiApiAdapters(input: Input): Omit void }) { const data = useData() const dialog = useDialog() - const sdk = useSDK() + const client = useClient() const route = useRoute() const toast = useToast() const [pending, setPending] = createSignal(false) const fork = async (messageID?: string) => { setPending(true) - const result = await sdk.api.session.fork({ sessionID: props.sessionID, messageID }).catch((error) => { + const result = await client.api.session.fork({ sessionID: props.sessionID, messageID }).catch((error) => { toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }) return undefined }) diff --git a/packages/tui/src/routes/session/dialog-message.tsx b/packages/tui/src/routes/session/dialog-message.tsx index 889d19f716..8935524634 100644 --- a/packages/tui/src/routes/session/dialog-message.tsx +++ b/packages/tui/src/routes/session/dialog-message.tsx @@ -3,7 +3,7 @@ import { useData } from "../../context/data" import { DialogSelect } from "../../ui/dialog-select" import { useClipboard } from "../../context/clipboard" import { useToast } from "../../ui/toast" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { errorMessage } from "../../util/error" import { DialogFork } from "./dialog-fork" import type { PromptInfo } from "../../prompt/history" @@ -16,7 +16,7 @@ export function DialogMessage(props: { const data = useData() const clipboard = useClipboard() const toast = useToast() - const sdk = useSDK() + const client = useClient() const message = createMemo(() => data.session.message.get(props.sessionID, props.messageID)) return ( @@ -45,7 +45,7 @@ export function DialogMessage(props: { pasted: [], }) } - void sdk.api.session.revert + void client.api.session.revert .stage({ sessionID: props.sessionID, messageID: props.messageID }) .catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })) dialog.clear() diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx index 922c8b1afd..9f41e708a9 100644 --- a/packages/tui/src/routes/session/form.tsx +++ b/packages/tui/src/routes/session/form.tsx @@ -7,7 +7,7 @@ import { selectedForeground, useTheme } from "../../context/theme" import { tint } from "../../theme/color" import type { FormField, FormValue } from "@opencode-ai/client" import type { FormWithLocation } from "../../context/data" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { useClipboard } from "../../context/clipboard" import { SplitBorder } from "../../ui/border" import { useToast } from "../../ui/toast" @@ -146,7 +146,7 @@ function requestOptions(form: FormWithLocation) { } export function FormPrompt(props: { form: FormWithLocation }) { - const sdk = useSDK() + const client = useClient() const { theme } = useTheme() const renderer = useRenderer() const dimensions = useTerminalDimensions() @@ -297,7 +297,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { } function replySingle(field: Field, value: FormValue) { - sdk.api.form + client.api.form .reply( { sessionID: props.form.sessionID, @@ -479,7 +479,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { } function cancel() { - void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id }, requestOptions(props.form)) + void client.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id }, requestOptions(props.form)) } function openExternal() { @@ -531,7 +531,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { setStore("error", validateValue(invalid, store.answers[invalid.key]) ?? "Invalid answer") return } - sdk.api.form + client.api.form .reply( { sessionID: props.form.sessionID, @@ -582,7 +582,7 @@ export function FormPrompt(props: { form: FormWithLocation }) { group: "Form", cmd: () => { if (textual()) { - void sdk.api.form.cancel( + void client.api.form.cancel( { sessionID: props.form.sessionID, formID: props.form.id }, requestOptions(props.form), ) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 788bbbe8e1..0da67d4a08 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -41,7 +41,7 @@ import { Locale } from "../../util/locale" import { FilePath } from "../../ui/file-path" import { webSearchProviderLabel } from "../../util/tool-display" import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { useEditorContext } from "../../context/editor" import { openEditor } from "../../editor" import { useDialog } from "../../ui/dialog" @@ -215,7 +215,7 @@ export function Session() { const scrollAcceleration = createMemo(() => getScrollAcceleration(config)) const toast = useToast() - const sdk = useSDK() + const client = useClient() const editor = useEditorContext() const rows = createSessionRows(() => route.sessionID) @@ -389,7 +389,7 @@ export function Session() { aliases: ["summarize"], }, run: () => { - void sdk.api.session.compact({ sessionID: route.sessionID }) + void client.api.session.compact({ sessionID: route.sessionID }) dialog.clear() }, }, @@ -417,7 +417,7 @@ export function Session() { dialog.clear() return } - void sdk.api.session.revert + void client.api.session.revert .stage({ sessionID: route.sessionID, messageID: message.id }) .catch((error) => toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })) prompt?.set({ @@ -445,7 +445,7 @@ export function Session() { slash: { name: "redo" }, run: () => { void (async () => { - const error = await sdk.api.session.revert.clear({ sessionID: route.sessionID }).then( + const error = await client.api.session.revert.clear({ sessionID: route.sessionID }).then( () => undefined, (error) => error, ) @@ -722,11 +722,11 @@ export function Session() { : await (async () => { if (options.debug) { const events: { readonly created: number }[] = [] - for await (const event of sdk.api.session.log({ sessionID: sessionData.id, follow: false })) { + for await (const event of client.api.session.log({ sessionID: sessionData.id, follow: false })) { if (event.type !== "log.synced") events.push(event) } // Durable events stay in aggregate order even when their wall-clock timestamps differ. - sdk.connection.internal.history().forEach((event) => { + client.connection.internal.history().forEach((event) => { const index = events.findIndex((item) => item.created > event.created) if (index === -1) { events.push(event) @@ -740,7 +740,7 @@ export function Session() { const messages: unknown[] = [] let cursor: string | undefined do { - const page = await sdk.api.message.list( + const page = await client.api.message.list( cursor ? { sessionID: sessionData.id, limit: 200, cursor } : { sessionID: sessionData.id, limit: 200, order: "asc" }, @@ -776,7 +776,7 @@ export function Session() { category: "Session", hidden: true, run: () => { - void sdk.api.session.background({ sessionID: route.sessionID }) + void client.api.session.background({ sessionID: route.sessionID }) dialog.clear() }, }, @@ -1380,7 +1380,7 @@ function RevertMessage(props: { const ctx = use() const { theme } = useTheme() const route = useRouteData("session") - const sdk = useSDK() + const client = useClient() const toast = useToast() const renderer = useRenderer() const [hover, setHover] = createSignal(false) @@ -1392,7 +1392,7 @@ function RevertMessage(props: { onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return void (async () => { - const error = await sdk.api.session.revert.clear({ sessionID: route.sessionID }).then( + const error = await client.api.session.revert.clear({ sessionID: route.sessionID }).then( () => undefined, (error) => error, ) diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index a9382c85d3..d20cf3d58c 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -5,7 +5,7 @@ import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/s import type { TextareaRenderable } from "@opentui/core" import { useTheme, selectedForeground } from "../../context/theme" import type { PermissionV2Request } from "@opencode-ai/client" -import { useSDK } from "../../context/sdk" +import { useClient } from "../../context/client" import { SplitBorder } from "../../ui/border" import { useData } from "../../context/data" import { filetype } from "../../util/filetype" @@ -135,7 +135,7 @@ function TextBody(props: { title: string; description?: string; icon?: string }) } export function PermissionPrompt(props: { request: PermissionV2Request; directory?: string }) { - const sdk = useSDK() + const client = useClient() const data = useData() const [store, setStore] = createStore({ stage: "permission" as PermissionStage, @@ -187,7 +187,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director onSelect={(option) => { setStore("stage", "permission") if (option === "cancel") return - void sdk.api.permission.reply({ + void client.api.permission.reply({ sessionID: props.request.sessionID, reply: "always", requestID: props.request.id, @@ -198,7 +198,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director { - void sdk.api.permission.reply({ + void client.api.permission.reply({ sessionID: props.request.sessionID, reply: "reject", requestID: props.request.id, @@ -444,14 +444,14 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director setStore("stage", "reject") return } - void sdk.api.permission.reply({ + void client.api.permission.reply({ sessionID: props.request.sessionID, reply: "reject", requestID: props.request.id, }) return } - void sdk.api.permission.reply({ + void client.api.permission.reply({ sessionID: props.request.sessionID, reply: "once", requestID: props.request.id, diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index 3827333856..d5b1736a5a 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -4,7 +4,7 @@ import { createTestRenderer } from "@opentui/core/testing" import { Effect } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Global } from "@opencode-ai/core/global" -import { createEventStream, createFetch, directory, json } from "./fixture/tui-sdk" +import { createEventStream, createFetch, directory, json } from "./fixture/tui-client" test("SIGHUP clears title and disposes scoped resources once", async () => { const setup = await createTestRenderer({ width: 80, height: 24, useThread: false }) diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index 6f8399fbdd..1fa253b0fd 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -1,14 +1,19 @@ import { describe, expect, test } from "bun:test" import Notifications from "../../../../src/feature-plugins/system/notifications" import type { OpenCodeEvent, PermissionAsked, QuestionAsked } from "@opencode-ai/client" -import type { Session } from "@opencode-ai/sdk/v2" -import type { TuiAttentionNotifyInput } from "@opencode-ai/plugin/tui" +import type { TuiAttentionNotifyInput, TuiPluginApi } from "@opencode-ai/plugin/tui" import { createTuiPluginApi } from "../../../fixture/tui-plugin" +type Session = NonNullable> + async function setup() { const notifications: TuiAttentionNotifyInput[] = [] const handlers = new Map void)[]>() - const session = (id: string, title: string, parentID?: string): Session => ({ + const session = ( + id: string, + title: string, + parentID?: string, + ): Session => ({ id, title, slug: id, diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 0e4102d30c..2671ba1e22 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -6,10 +6,10 @@ import { SessionMessage } from "@opencode-ai/core/session/message" import { EventV2 } from "@opencode-ai/core/event" import { onMount } from "solid-js" import { ProjectProvider } from "../../../src/context/project" -import { SDKProvider, useSDK } from "../../../src/context/sdk" +import { ClientProvider, useClient } from "../../../src/context/client" import { DataProvider, useData } from "../../../src/context/data" import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows" -import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk" +import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client" import { TestTuiContexts } from "../../fixture/tui-environment" const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [ @@ -90,13 +90,13 @@ test("refreshes resources into reactive getters", async () => { const app = await testRender(() => ( - + - + )) @@ -148,13 +148,13 @@ test("applies absolute usage events to session info", async () => { const app = await testRender(() => ( - + - + )) @@ -233,13 +233,13 @@ test("truncates committed revert messages without changing lifetime usage", asyn const app = await testRender(() => ( - + - + )) @@ -371,13 +371,13 @@ test("updates session location when moved", async () => { const app = await testRender(() => ( - + - + )) @@ -430,13 +430,13 @@ test("restores running manual compaction before applying live deltas", async () const app = await testRender(() => ( - + - + )) @@ -514,23 +514,23 @@ test("reconnects the event stream and bootstraps fresh data", async () => { }) }, events) let data!: ReturnType - let sdk!: ReturnType + let client!: ReturnType function Probe() { data = useData() - sdk = useSDK() + client = useClient() return } const app = await testRender(() => ( - + - + )) @@ -539,15 +539,15 @@ test("reconnects the event stream and bootstraps fresh data", async () => { await wait(() => data.session.status("session-stale") === "running") await data.session.message.refresh("session-stale") expect(data.session.message.get("session-stale", "message-stale")?.id).toBe("message-stale") - expect(sdk.connection.status()).toBe("connected") - expect(sdk.connection.attempt()).toBe(0) + expect(client.connection.status()).toBe("connected") + expect(client.connection.attempt()).toBe(0) events.disconnect() - await wait(() => sdk.connection.status() === "reconnecting") - expect(sdk.connection.attempt()).toBe(1) - expect(sdk.connection.error()).toBe("Event stream disconnected") + await wait(() => client.connection.status() === "reconnecting") + expect(client.connection.attempt()).toBe(1) + expect(client.connection.error()).toBe("Event stream disconnected") - await wait(() => requests.active === 2 && sdk.connection.status() === "connected", 4000) + await wait(() => requests.active === 2 && client.connection.status() === "connected", 4000) resolveActive(json({ data: { "session-new": { type: "running" } } })) await wait(() => data.location.model.list()?.[0]?.id === "model-2", 4000) @@ -565,9 +565,9 @@ test("reconnects the event stream and bootstraps fresh data", async () => { await wait(() => data.session.status("session-new") === "running") expect(requests.event).toBe(2) expect(requests.message).toBe(2) - expect(sdk.connection.status()).toBe("connected") - expect(sdk.connection.attempt()).toBe(0) - expect(sdk.connection.error()).toBeUndefined() + expect(client.connection.status()).toBe("connected") + expect(client.connection.attempt()).toBe(0) + expect(client.connection.error()).toBeUndefined() } finally { app.renderer.destroy() } @@ -588,13 +588,13 @@ test("completes exploration when a queued prompt is promoted", async () => { const app = await testRender(() => ( - + - + )) @@ -668,13 +668,13 @@ test("removes committed revert messages from local state", async () => { const app = await testRender(() => ( - + - + )) @@ -733,34 +733,34 @@ test("distinguishes initial connection from reconnection", async () => { const calls = createFetch((url) => { if (url.pathname === "/api/event") return eventResponse() }) - let sdk!: ReturnType + let client!: ReturnType function Probe() { - sdk = useSDK() + client = useClient() return } const app = await testRender(() => ( - + - + )) try { await wait(() => stream !== undefined) - expect(sdk.connection.status()).toBe("connecting") + expect(client.connection.status()).toBe("connecting") connect() - await wait(() => sdk.connection.status() === "connected") + await wait(() => client.connection.status() === "connected") disconnect() - await wait(() => sdk.connection.status() === "reconnecting") + await wait(() => client.connection.status() === "reconnecting") } finally { app.renderer.destroy() } @@ -811,13 +811,13 @@ test("tracks session status from active sessions and execution events", async () const app = await testRender(() => ( - + - + )) @@ -1180,13 +1180,13 @@ test("restores queued compaction from durable pending input", async () => { const app = await testRender(() => ( - + - + )) @@ -1295,13 +1295,13 @@ test("refreshes integrations after integration updates", async () => { const app = await testRender(() => ( - + - + )) @@ -1351,13 +1351,13 @@ test("refreshes MCP resources after catalog updates", async () => { const app = await testRender(() => ( - + - + )) @@ -1400,13 +1400,13 @@ test("refreshes effective catalog data after catalog updates", async () => { const app = await testRender(() => ( - + - + )) @@ -1448,13 +1448,13 @@ test("refreshes agents after agent updates", async () => { const app = await testRender(() => ( - + - + )) @@ -1492,13 +1492,13 @@ test("refreshes references after updates", async () => { const app = await testRender(() => ( - + - + )) @@ -1547,13 +1547,13 @@ test("keeps shell state scoped to location", async () => { const app = await testRender(() => ( - + - + )) @@ -1593,28 +1593,28 @@ test("adds and dismisses permission requests from live events", async () => { const events = createEventStream() const calls = createFetch(undefined, events) let data!: ReturnType - let sdk!: ReturnType + let client!: ReturnType function Probe() { data = useData() - sdk = useSDK() + client = useClient() return } const app = await testRender(() => ( - + - + )) try { - await wait(() => sdk.connection.status() === "connected") + await wait(() => client.connection.status() === "connected") emitEvent(events, { id: "evt_permission_asked_1", created: 0, @@ -1681,13 +1681,13 @@ test("reconciles all pending permission requests when the event stream reconnect const app = await testRender(() => ( - + - + )) @@ -1715,28 +1715,28 @@ test("adds, dismisses, and refreshes form requests", async () => { }) }, events) let data!: ReturnType - let sdk!: ReturnType + let client!: ReturnType function Probe() { data = useData() - sdk = useSDK() + client = useClient() return } const app = await testRender(() => ( - + - + )) try { - await wait(() => sdk.connection.status() === "connected") + await wait(() => client.connection.status() === "connected") emitEvent(events, { id: "evt_form_created_1", created: 0, @@ -1785,28 +1785,28 @@ test("tracks global forms by location", async () => { const calls = createFetch(undefined, events) const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" } let data!: ReturnType - let sdk!: ReturnType + let client!: ReturnType function Probe() { data = useData() - sdk = useSDK() + client = useClient() return } const app = await testRender(() => ( - + - + )) try { - await wait(() => sdk.connection.status() === "connected") + await wait(() => client.connection.status() === "connected") events.emit({ id: "evt_form_created_global_other", created: 0, @@ -1871,28 +1871,28 @@ test("refreshes global forms for the requested location", async () => { }) }, events) let data!: ReturnType - let sdk!: ReturnType + let client!: ReturnType function Probe() { data = useData() - sdk = useSDK() + client = useClient() return } const app = await testRender(() => ( - + - + )) try { - await wait(() => sdk.connection.status() === "connected" && requests.length > 0) + await wait(() => client.connection.status() === "connected" && requests.length > 0) requests.length = 0 await data.session.form.refresh("global", { directory }) @@ -1957,13 +1957,13 @@ test("refreshes global forms once per loaded location after reconnect", async () const app = await testRender(() => ( - + - + )) @@ -2025,13 +2025,13 @@ test("reconciles all pending form requests when the event stream reconnects", as const app = await testRender(() => ( - + - + )) @@ -2078,13 +2078,13 @@ test("settles pending tools when a live failure arrives", async () => { const app = await testRender(() => ( - + - + )) @@ -2225,13 +2225,13 @@ test("renders admitted prompts immediately and tracks them until promoted", asyn const app = await testRender(() => ( - + - + )) @@ -2318,13 +2318,13 @@ test("projects live instruction updates with their message ID", async () => { const app = await testRender(() => ( - + - + )) @@ -2387,13 +2387,13 @@ async function mountData(parents: Record, costs: Record ( - + - + )) await mounted diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx index 494837d9a0..a4f2ec0ef6 100644 --- a/packages/tui/test/cli/tui/diff-viewer.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx @@ -4,17 +4,16 @@ import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" import { DiffRenderable, type Renderable, ScrollBoxRenderable } from "@opentui/core" import { testRender, useRenderer } from "@opentui/solid" import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui" -import type { Session } from "@opencode-ai/sdk/v2" import { ThemeProvider } from "../../../src/context/theme" import { ConfigProvider } from "../../../src/config" -import { SDKProvider } from "../../../src/context/sdk" +import { ClientProvider } from "../../../src/context/client" import { TuiKeybind } from "../../../src/config/keybind" import { OpencodeKeymapProvider } from "../../../src/keymap" import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" import { TestTuiContexts } from "../../fixture/tui-environment" -import { createApi, createClient, createEventStream, createFetch, json } from "../../fixture/tui-sdk" +import { createApi, createEventStream, createFetch, json } from "../../fixture/tui-client" test("closing the diff viewer returns to the route it opened from", async () => { const viewer = await renderDiffViewer([]) @@ -111,7 +110,6 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: let current = initialRoute ?? startRoute let renderDiff: TuiRouteDefinition["render"] | undefined let vcsDiffInput: unknown - let sessionDiffInput: unknown const config = createTuiResolvedConfig() const transport = createFetch((url) => { if (url.pathname !== "/api/vcs/diff") return @@ -135,14 +133,6 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: } const base = createTuiPluginApi({ keymap, - client: { - session: { - diff: async (input: unknown) => { - sessionDiffInput = input - return { data: [] } - }, - }, - } as unknown as TuiPluginApi["client"], state: { session: { get: () => session, @@ -170,7 +160,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: return ( - + @@ -178,7 +168,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: - + ) } @@ -190,7 +180,6 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20, initialRoute?: commands, current: () => current, vcsDiffInput: () => vcsDiffInput, - sessionDiffInput: () => sessionDiffInput, } } @@ -217,7 +206,7 @@ const session = { created: 0, updated: 0, }, -} satisfies Session +} satisfies NonNullable> test("branch diff source requests branch VCS diff", async () => { const viewer = await renderDiffViewer([], 20, { @@ -234,24 +223,6 @@ test("branch diff source requests branch VCS diff", async () => { mode: "branch", context: "12", }) - expect(viewer.sessionDiffInput()).toBeUndefined() - } finally { - viewer.app.renderer.destroy() - } -}) - -test("last-turn diff source requests session diff", async () => { - const viewer = await renderDiffViewer([], 20, { - name: "diff", - params: { mode: "last-turn", sessionID: "session-1", messageID: "message-1", returnRoute: startRoute }, - }) - try { - expect(viewer.current()).toEqual({ - name: "diff", - params: { mode: "last-turn", sessionID: "session-1", messageID: "message-1", returnRoute: startRoute }, - }) - expect(viewer.sessionDiffInput()).toEqual({ sessionID: "session-1", messageID: "message-1" }) - expect(viewer.vcsDiffInput()).toBeUndefined() } finally { viewer.app.renderer.destroy() } diff --git a/packages/tui/test/cli/tui/form.test.tsx b/packages/tui/test/cli/tui/form.test.tsx index b4ab1f57f3..c1c83e8723 100644 --- a/packages/tui/test/cli/tui/form.test.tsx +++ b/packages/tui/test/cli/tui/form.test.tsx @@ -7,7 +7,7 @@ import path from "node:path" import { onCleanup } from "solid-js" import { ClipboardProvider } from "../../../src/context/clipboard" import type { FormWithLocation } from "../../../src/context/data" -import { SDKProvider } from "../../../src/context/sdk" +import { ClientProvider } from "../../../src/context/client" import { ThemeProvider } from "../../../src/context/theme" import { ConfigProvider } from "../../../src/config" import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap" @@ -15,7 +15,7 @@ import { ToastProvider } from "../../../src/ui/toast" import { tmpdir } from "../../fixture/fixture" import { TestTuiContexts } from "../../fixture/tui-environment" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk" +import { createApi, createEventStream, createFetch } from "../../fixture/tui-client" async function mountForm(root: string, width = 80) { const state = path.join(root, "state") @@ -75,13 +75,13 @@ async function mountForm(root: string, width = 80) { > - + Promise.resolve({}) }}> - + diff --git a/packages/tui/test/cli/tui/prompt-submit-race.test.ts b/packages/tui/test/cli/tui/prompt-submit-race.test.ts index c758762bfb..640561185a 100644 --- a/packages/tui/test/cli/tui/prompt-submit-race.test.ts +++ b/packages/tui/test/cli/tui/prompt-submit-race.test.ts @@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test" // Before the fix, two concurrent `submit()` calls (e.g. a double-pressed // Enter, or the input's native onSubmit racing another dispatch) each // passed the `if (!store.prompt.text) return false` guard, each -// `await sdk.client.session.create(...)`, and each only captured +// `await client.api.session.create(...)`, and each only captured // `inputText = store.prompt.text` AFTER that await. The first invocation // finished, sent the prompt, and cleared the store; the second invocation, // now past its await, read the cleared store and sent an empty prompt to a diff --git a/packages/tui/test/cli/tui/use-event.test.tsx b/packages/tui/test/cli/tui/use-event.test.tsx index 94194b122a..68b5f275c4 100644 --- a/packages/tui/test/cli/tui/use-event.test.tsx +++ b/packages/tui/test/cli/tui/use-event.test.tsx @@ -2,12 +2,11 @@ import { describe, expect, test } from "bun:test" import type { OpenCodeClient, OpenCodeEvent } from "@opencode-ai/client" import { testRender } from "@opentui/solid" -import type { OpencodeClient } 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 { ClientProvider, useClient } from "../../../src/context/client" import { useEvent } from "../../../src/context/event" -import { createApi, createClient, createEventStream, createFetch } from "../../fixture/tui-sdk" +import { createApi, createEventStream, createFetch } from "../../fixture/tui-client" import { TestTuiContexts } from "../../fixture/tui-environment" import type { LogLevel, LogSink } from "../../../src/context/log" @@ -54,7 +53,7 @@ function update(version: string): OpenCodeEvent { } async function mount( - reconnect?: (attempt: number) => Promise<{ client: OpencodeClient; api: OpenCodeClient }>, + reconnect?: (attempt: number) => Promise<{ api: OpenCodeClient }>, log?: LogSink, ) { const events = createEventStream() @@ -62,7 +61,7 @@ async function mount( const seen: OpenCodeEvent[] = [] const workspaces: Array = [] let project!: ReturnType - let sdk!: ReturnType + let client!: ReturnType let done!: () => void const ready = new Promise((resolve) => { done = resolve @@ -70,12 +69,12 @@ async function mount( const app = await testRender(() => ( - + { project = ctx.project - sdk = ctx.sdk + client = ctx.client await project.sync() done() }} @@ -83,21 +82,21 @@ async function mount( workspaces={workspaces} /> - + )) await ready - return { app, events, emit: events.emit, project, sdk, seen, workspaces } + return { app, events, emit: events.emit, project, client, seen, workspaces } } function Probe(props: { seen: OpenCodeEvent[] workspaces: Array - onReady: (ctx: { project: ReturnType; sdk: ReturnType }) => void + onReady: (ctx: { project: ReturnType; client: ReturnType }) => void }) { const project = useProject() - const sdk = useSDK() + const client = useClient() const event = useEvent() onMount(() => { @@ -105,7 +104,7 @@ function Probe(props: { props.seen.push(evt) props.workspaces.push(workspace) }) - props.onReady({ project, sdk }) + props.onReady({ project, client }) }) return @@ -137,7 +136,7 @@ describe("useEvent", () => { { level: "debug", message: "event", - tags: { component: "sdk", type: "session.renamed", aggregateID: "ses_test", seq: 1 }, + tags: { component: "client", type: "session.renamed", aggregateID: "ses_test", seq: 1 }, }, ]) } finally { @@ -194,25 +193,24 @@ describe("useEvent", () => { const attempts: number[] = [] const replacementEvents = createEventStream() const replacementCalls = createFetch(undefined, replacementEvents) - const replacement = { client: createClient(replacementCalls.fetch), api: createApi(replacementCalls.fetch) } - const { app, events, sdk, seen } = await mount(async (attempt) => { + const replacement = { api: createApi(replacementCalls.fetch) } + const { app, events, client, seen } = await mount(async (attempt) => { attempts.push(attempt) return replacement }) try { - await wait(() => sdk.connection.status() === "connected") + await wait(() => client.connection.status() === "connected") // Reconnection only runs when the stream is down, never while connected. expect(attempts).toEqual([]) events.disconnect() - await wait(() => sdk.connection.status() === "connected" && attempts.length > 0) + await wait(() => client.connection.status() === "connected" && attempts.length > 0) replacementEvents.emit(event(vcs("rediscovered"), { directory: "/tmp/rediscovered" })) await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "rediscovered")) - expect(sdk.client).toBe(replacement.client) - expect(sdk.api).toBe(replacement.api) + expect(client.api).toBe(replacement.api) expect(attempts).toEqual([1]) - const history = sdk.connection.internal.history() + const history = client.connection.internal.history() expect(history.map((event) => [event.data.status, event.data.attempt])).toEqual([ ["connecting", 0], ["connected", 0], @@ -228,22 +226,22 @@ describe("useEvent", () => { test("keeps the current client when reconnection fails", async () => { let calls = 0 - const { app, events, sdk, seen } = await mount(async () => { + const { app, events, client, seen } = await mount(async () => { calls += 1 throw new Error("no server") }) try { - await wait(() => sdk.connection.status() === "connected") - const original = sdk.client + await wait(() => client.connection.status() === "connected") + const original = client.api events.disconnect() // Reconnection rejects; the loop retries against the last known transport, // which succeeds once the fixture accepts the reconnect. - await wait(() => calls > 0 && sdk.connection.status() === "connected") + await wait(() => calls > 0 && client.connection.status() === "connected") events.emit(event(vcs("recovered"), { directory: "/tmp/recovered" })) await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "recovered")) - expect(sdk.client).toBe(original) + expect(client.api).toBe(original) } finally { app.renderer.destroy() } diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-client.ts similarity index 96% rename from packages/tui/test/fixture/tui-sdk.ts rename to packages/tui/test/fixture/tui-client.ts index 8d0f72206f..e60f79e964 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-client.ts @@ -1,5 +1,4 @@ import { OpenCode, type OpenCodeEvent } from "@opencode-ai/client" -import { createOpencodeClient } from "@opencode-ai/sdk/v2" export const worktree = "/tmp/opencode" export const directory = `${worktree}/packages/tui` @@ -135,10 +134,6 @@ export function createFetch(override?: FetchHandler, events?: ReturnType