From 3660b79dbd44320b627fca462349b5fc26e28dc7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 29 Jun 2026 00:06:33 -0400 Subject: [PATCH] refactor(tui): migrate generated client actions --- .../tui/src/component/dialog-integration.tsx | 106 ++++---- .../tui/src/component/dialog-move-session.tsx | 33 ++- .../tui/src/component/dialog-session-list.tsx | 21 +- .../src/component/dialog-session-rename.tsx | 10 +- packages/tui/src/component/dialog-tag.tsx | 16 +- .../tui/src/component/prompt/autocomplete.tsx | 29 +-- packages/tui/src/component/prompt/index.tsx | 146 ++++++----- packages/tui/src/component/prompt/move.tsx | 19 +- packages/tui/src/context/data.tsx | 23 +- .../tui/src/routes/session/dialog-message.tsx | 12 +- packages/tui/src/routes/session/index.tsx | 228 +++++++++--------- .../tui/src/routes/session/permission.tsx | 8 +- packages/tui/src/routes/session/question.tsx | 10 +- 13 files changed, 343 insertions(+), 318 deletions(-) diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index 0939fedcda..20beb09ded 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -1,5 +1,6 @@ import { TextAttributes } from "@opentui/core" -import type { ConnectionInfo, IntegrationInfo, IntegrationOAuthMethod, IntegrationAttempt } from "@opencode-ai/sdk/v2" +import type { IntegrationsConnectOauthOutput } from "@opencode-ai/client" +import type { ConnectionInfo, IntegrationInfo, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2" import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js" import { useClipboard } from "../context/clipboard" import { useData } from "../context/data" @@ -22,6 +23,7 @@ const INTEGRATION_PRIORITY: Record = { } type ConnectMethod = Exclude +type IntegrationAttempt = IntegrationsConnectOauthOutput["data"] export function integrationOptions(list: IntegrationInfo[]) { return list.toSorted( @@ -109,11 +111,8 @@ function manageConnections( title: `Disconnect ${connection.label}`, value: connection.id, onSelect: () => { - void sdk.client.v2.credential - .remove( - { credentialID: connection.id, location: location(data) }, - { throwOnError: true }, - ) + void sdk.api.credentials + .remove({ credentialID: connection.id, location: location(data) }) .then(() => disconnected(integration.name, data, dialog, toast)) .catch(toast.error) }, @@ -124,11 +123,7 @@ function manageConnections( }) } -function selectMethod( - integration: IntegrationInfo, - methods: ConnectMethod[], - dialog: ReturnType, -) { +function selectMethod(integration: IntegrationInfo, methods: ConnectMethod[], dialog: ReturnType) { if (methods.length === 1) return openMethod(integration, methods[0], dialog) dialog.replace(() => ( , -) { +function openMethod(integration: IntegrationInfo, method: ConnectMethod, dialog: ReturnType) { if (method.type === "key") { dialog.replace(() => ) return @@ -168,21 +159,16 @@ function KeyMethod(props: { integration: IntegrationInfo; method: Extract { if (!key) return - void sdk.client.v2.integration.connect - .key( - { - integrationID: props.integration.id, - location: location(data), - key, - }, - { throwOnError: true }, - ) + void sdk.api.integrations + .connectKey({ + integrationID: props.integration.id, + location: location(data), + key, + }) .then(() => connected(props.integration.name, data, dialog, toast)) .catch((cause) => setError(message(cause))) }} - description={() => ( - {(value) => {value()}} - )} + description={() => {(value) => {value()}}} /> ) } @@ -208,25 +194,22 @@ function OAuthStarting(props: { const toast = useToast() onMount(() => { - void sdk.client.v2.integration.connect - .oauth( - { - integrationID: props.integration.id, - location: location(data), - methodID: props.method.id, - inputs: props.inputs, - }, - { throwOnError: true }, - ) + void sdk.api.integrations + .connectOauth({ + integrationID: props.integration.id, + location: location(data), + methodID: props.method.id, + inputs: props.inputs, + }) .then((result) => { - if (result.data.data.mode === "code") { + if (result.data.mode === "code") { dialog.replace(() => ( - + )) return } dialog.replace(() => ( - + )) }) .catch((cause) => { @@ -265,10 +248,10 @@ function OAuthAuto(props: { integration: IntegrationInfo; title: string; attempt })) const poll = () => { - void sdk.client.v2.integration.attempt - .status({ attemptID: props.attempt.attemptID, location: location(data) }, { throwOnError: true }) + void sdk.api.integrations + .attemptStatus({ attemptID: props.attempt.attemptID, location: location(data) }) .then((result) => { - const status = result.data.data + const status = result.data if (status.status === "pending") { timer = setTimeout(poll, 500) return @@ -292,7 +275,7 @@ function OAuthAuto(props: { integration: IntegrationInfo; title: string; attempt onCleanup(() => { if (timer) clearTimeout(timer) if (settled) return - void sdk.client.v2.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) + void sdk.api.integrations.attemptCancel({ attemptID: props.attempt.attemptID, location: location(data) }) }) return ( @@ -317,7 +300,7 @@ function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt onCleanup(() => { if (settled) return - void sdk.client.v2.integration.attempt.cancel({ attemptID: props.attempt.attemptID, location: location(data) }) + void sdk.api.integrations.attemptCancel({ attemptID: props.attempt.attemptID, location: location(data) }) }) return ( @@ -326,11 +309,8 @@ function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt placeholder="Authorization code" onConfirm={(code) => { if (!code) return - void sdk.client.v2.integration.attempt - .complete( - { attemptID: props.attempt.attemptID, location: location(data), code }, - { throwOnError: true }, - ) + void sdk.api.integrations + .attemptComplete({ attemptID: props.attempt.attemptID, location: location(data), code }) .then(() => { settled = true return connected(props.integration.name, data, dialog, toast) @@ -348,13 +328,7 @@ function OAuthCode(props: { integration: IntegrationInfo; title: string; attempt ) } -function OAuthView(props: { - title: string - url?: string - instructions?: string - message: string - copy?: boolean -}) { +function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) { const dialog = useDialog() const { theme } = useTheme() return ( @@ -371,7 +345,9 @@ function OAuthView(props: { {(url) => ( - {(instructions) => {instructions()}} + + {(instructions) => {instructions()}} + )} @@ -436,7 +412,11 @@ async function connected( dialog: ReturnType, toast: ReturnType, ) { - await Promise.all([data.location.integration.refresh(), data.location.model.refresh(), data.location.provider.refresh()]) + await Promise.all([ + data.location.integration.refresh(), + data.location.model.refresh(), + data.location.provider.refresh(), + ]) toast.show({ variant: "success", message: `Connected ${name}` }) dialog.clear() } @@ -447,7 +427,11 @@ async function disconnected( dialog: ReturnType, toast: ReturnType, ) { - await Promise.all([data.location.integration.refresh(), data.location.model.refresh(), data.location.provider.refresh()]) + await Promise.all([ + data.location.integration.refresh(), + data.location.model.refresh(), + data.location.provider.refresh(), + ]) toast.show({ variant: "success", message: `Disconnected ${name}` }) dialog.clear() } diff --git a/packages/tui/src/component/dialog-move-session.tsx b/packages/tui/src/component/dialog-move-session.tsx index b4fb2f2b45..200c18d97a 100644 --- a/packages/tui/src/component/dialog-move-session.tsx +++ b/packages/tui/src/component/dialog-move-session.tsx @@ -11,6 +11,7 @@ import { abbreviateHome } from "../runtime" import { useTuiPaths } from "../context/runtime" import { Locale } from "../util/locale" import { errorMessage } from "../util/error" +import { isRecord } from "../util/record" import { useToast } from "../ui/toast" import { useCommandShortcut } from "../keymap" import { useProject } from "../context/project" @@ -74,10 +75,10 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { () => (props.initialRemoving ? undefined : props.projectID), async (projectID, info): Promise => { try { - await sdk.client.v2.projectCopy.refresh( - { projectID, location: { directory: projectContext.instance.directory() || paths.cwd } }, - { throwOnError: true }, - ) + await sdk.api.projectCopies.refresh({ + projectID, + location: { directory: projectContext.instance.directory() || paths.cwd }, + }) const directories = await sdk.client.project.directories({ projectID }, { throwOnError: true }) setLoadError(undefined) return directories.data ?? [] @@ -221,18 +222,21 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { setToDelete(undefined) setRemoving(selected.directory) setWorking(true) - const result = await sdk.client.v2.projectCopy + const error = await sdk.api.projectCopies .remove({ projectID: props.projectID, location: { directory: projectContext.instance.directory() || paths.cwd }, directory: selected.directory, force: false, }) - .catch((error) => ({ error })) - if (result.error) { + .then( + () => undefined, + (error) => error, + ) + if (error) { setRemoving(undefined) setWorking(false) - if ("data" in result.error && result.error.data.forceRequired) { + if (isRecord(error) && isRecord(error.data) && error.data.forceRequired === true) { const status = await sdk.client.vcs.status({ directory: selected.directory }).catch(() => undefined) const choice = await DialogWorkspaceFileChanges.show(dialog, status?.data ?? [], { title: "Delete working copy?", @@ -243,19 +247,22 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { return } reopen(selected.directory) - const forced = await sdk.client.v2.projectCopy + const forcedError = await sdk.api.projectCopies .remove({ projectID: props.projectID, location: { directory: projectContext.instance.directory() || paths.cwd }, directory: selected.directory, force: true, }) - .catch((error) => ({ error })) - if (forced.error) { + .then( + () => undefined, + (error) => error, + ) + if (forcedError) { toast.show({ variant: "error", title: "Failed to delete project copy", - message: errorMessage(forced.error), + message: errorMessage(forcedError), }) reopen() return @@ -269,7 +276,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) { toast.show({ variant: "error", title: "Failed to delete project copy", - message: errorMessage(result.error), + message: errorMessage(error), }) return } diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 771ab0eca5..fc1915e398 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -31,11 +31,16 @@ export function DialogSessionList() { const [searchResults] = createResource(search, async (query) => { if (!query) return - const response = await sdk.client.v2.session.list( - { search: query, limit: 50, order: "desc" }, - { throwOnError: true }, - ) - return { query, sessions: response.data.data } + const location = data.location.default() + const response = await sdk.api.sessions.list({ + search: query, + limit: 50, + order: "desc", + directory: location.directory, + workspace: location.workspaceID, + }) + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; session list UI reuses legacy mutable session types. + return { query, sessions: structuredClone(response.data) as SessionV2Info[] } }) const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) @@ -59,7 +64,11 @@ export function DialogSessionList() { const options = createMemo(() => { const today = new Date().toDateString() - const sessionMap = new Map(sessions().filter((session) => !session.parentID).map((session) => [session.id, session])) + const sessionMap = new Map( + sessions() + .filter((session) => !session.parentID) + .map((session) => [session.id, session]), + ) const pinned = local.session.pinned().filter((sessionID) => sessionMap.has(sessionID)) const pinnedSet = new Set(pinned) const slotByID = new Map(local.session.slots().map((sessionID, index) => [sessionID, index + 1])) diff --git a/packages/tui/src/component/dialog-session-rename.tsx b/packages/tui/src/component/dialog-session-rename.tsx index 72dfb8b11d..1b9e3372cd 100644 --- a/packages/tui/src/component/dialog-session-rename.tsx +++ b/packages/tui/src/component/dialog-session-rename.tsx @@ -17,11 +17,15 @@ export function DialogSessionRename(props: { sessionID: string; currentTitle?: s onConfirm={(value) => { const title = value.trim() if (!title) return - void sdk.client.v2.session - .rename({ sessionID: props.sessionID, title }, { throwOnError: true }) + void sdk.api.sessions + .rename({ sessionID: props.sessionID, title }) .then(() => dialog.clear()) .catch((error) => - toast.show({ message: `Failed to rename session: ${errorMessage(error)}`, variant: "error", duration: 5000 }), + toast.show({ + message: `Failed to rename session: ${errorMessage(error)}`, + variant: "error", + duration: 5000, + }), ) }} onCancel={() => dialog.clear()} diff --git a/packages/tui/src/component/dialog-tag.tsx b/packages/tui/src/component/dialog-tag.tsx index 39560d5eac..2b3dc83524 100644 --- a/packages/tui/src/component/dialog-tag.tsx +++ b/packages/tui/src/component/dialog-tag.tsx @@ -17,13 +17,15 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) { const [files] = createResource( () => [store.filter], async () => { - const result = await sdk.client.find.files({ - query: store.filter, - workspace: project.workspace.current(), - }) - if (result.error) return [] - const sliced = (result.data ?? []).slice(0, 5) - return sliced + const result = await sdk.api.files + .find({ + query: store.filter, + type: "file", + limit: 5, + location: { workspace: project.workspace.current() }, + }) + .catch(() => undefined) + return result?.data.map((item) => item.path) ?? [] }, ) diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 28ebef05dc..38e387e499 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -320,29 +320,26 @@ export function Autocomplete(props: { if (referenceMatch()) return [] const { lineRange, baseQuery } = extractLineRange(input.query ?? "") - // Get files from SDK - const result = await sdk.client.v2.fs.find({ - query: baseQuery, - limit: "20", - location: { - directory: input.location?.directory, - workspace: input.location?.workspaceID ?? project.workspace.current(), - }, - }) + const result = await sdk.api.files + .find({ + query: baseQuery, + limit: 20, + location: { + directory: input.location?.directory, + workspace: input.location?.workspaceID ?? project.workspace.current(), + }, + }) + .catch(() => undefined) const options: AutocompleteOption[] = [] // Add file options. Trust the order returned by fff (frecency, fuzzy // score, filename bonus, etc. are already factored in). - if (!result.error && result.data) { + if (result) { const width = props.anchor().width - 4 options.push( - ...result.data.data.map((item): AutocompleteOption => { - const { filename, part } = createFilePart( - item, - path.join(result.data.location.directory, item.path), - lineRange, - ) + ...result.data.map((item): AutocompleteOption => { + const { filename, part } = createFilePart(item, path.join(result.location.directory, item.path), lineRange) return { display: Locale.truncateMiddle(filename, width), value: filename, diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index a48074cb4e..c8dc9202cb 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -37,7 +37,7 @@ import { usePromptStash } from "../../prompt/stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2" +import type { AssistantMessage, FilePart, SessionV2Info, UserMessage } from "@opencode-ai/sdk/v2" import { Locale } from "../../util/locale" import { errorMessage } from "../../util/error" import { createColors, createFrames } from "../../ui/spinner" @@ -158,11 +158,12 @@ export function Prompt(props: PromptProps) { const dialog = useDialog() const toast = useToast() const status = createMemo(() => data.session.status(props.sessionID ?? "")) - const activeSubagents = createMemo(() => - data.session - .list() - .filter((session) => session.parentID === props.sessionID && data.session.status(session.id) === "running") - .length, + const activeSubagents = createMemo( + () => + data.session + .list() + .filter((session) => session.parentID === props.sessionID && data.session.status(session.id) === "running") + .length, ) const history = usePromptHistory() const stash = usePromptStash() @@ -407,7 +408,7 @@ export function Prompt(props: PromptProps) { }, 5000) if (store.interrupt >= 2) { - void sdk.client.v2.session.interrupt({ + void sdk.api.sessions.interrupt({ sessionID: props.sessionID, }) setStore("interrupt", 0) @@ -992,18 +993,23 @@ export function Prompt(props: PromptProps) { const directory = await move.getDirectory(store.prompt.input) if (move.pending() && !directory) return false finishMoveProgress = Boolean(move.progress()) + const location = data.location.default() - const res = await sdk.client.v2.session.create({ - location: directory ? { directory, workspaceID } : undefined, - agent: agent.id, - model: { - providerID: selectedModel.providerID, - id: selectedModel.modelID, - variant, - }, - }) + const created = await sdk.api.sessions + .create({ + location: directory + ? { directory, workspaceID } + : { directory: location.directory, workspaceID: workspaceID ?? location.workspaceID }, + agent: agent.id, + model: { + providerID: selectedModel.providerID, + id: selectedModel.modelID, + variant, + }, + }) + .catch(() => undefined) - if (res.error) { + if (!created) { if (finishMoveProgress) move.finishSubmit() toast.show({ message: "Creating a session failed. Open console for more details.", @@ -1013,8 +1019,9 @@ export function Prompt(props: PromptProps) { return true } - sessionID = res.data.data.id - session = res.data.data + sessionID = created.id + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; prompt state still uses legacy mutable session types. + session = structuredClone(created) as SessionV2Info } const inputText = expandTrackedPastedText( @@ -1090,65 +1097,70 @@ export function Prompt(props: PromptProps) { session = data.session.get(sessionID) } if (session?.agent !== agent.id) { - await sdk.client.v2.session.switchAgent({ sessionID, agent: agent.id }, { throwOnError: true }) + await sdk.api.sessions.switchAgent({ sessionID, agent: agent.id }) } if ( session?.model?.providerID !== selectedModel.providerID || session.model.id !== selectedModel.modelID || session.model.variant !== variant ) { - await sdk.client.v2.session.switchModel( - { - sessionID, - model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant }, - }, - { throwOnError: true }, - ) + await sdk.api.sessions.switchModel({ + sessionID, + model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant }, + }) } if (session?.revert) { - const revertResult = await sdk.client.v2.session.revert.commit({ sessionID }) - if (revertResult.error) { - toast.show({ title: "Failed to commit revert", message: errorMessage(revertResult.error), variant: "error" }) + const error = await sdk.api.sessions.commit({ sessionID }).then( + () => undefined, + (error) => error, + ) + if (error) { + toast.show({ title: "Failed to commit revert", message: errorMessage(error), variant: "error" }) return false } } - const result = await sdk.client.v2.session.prompt({ - sessionID, - prompt: { - text: [...editorParts.map((part) => part.text), inputText].filter(Boolean).join("\n\n"), - files: nonTextParts.flatMap((part) => - part.type === "file" - ? [ - { - uri: part.url, - name: part.filename, - source: part.source - ? { - start: part.source.text.start, - end: part.source.text.end, - text: part.source.text.value, - } - : undefined, - }, - ] - : [], - ), - agents: nonTextParts.flatMap((part) => - part.type === "agent" - ? [ - { - name: part.name, - source: part.source - ? { start: part.source.start, end: part.source.end, text: part.source.value } - : undefined, - }, - ] - : [], - ), - }, - }) - if (result.error) { - toast.show({ title: "Failed to send prompt", message: errorMessage(result.error), variant: "error" }) + const error = await sdk.api.sessions + .prompt({ + sessionID, + prompt: { + text: [...editorParts.map((part) => part.text), inputText].filter(Boolean).join("\n\n"), + files: nonTextParts.flatMap((part) => + part.type === "file" + ? [ + { + uri: part.url, + name: part.filename, + source: part.source + ? { + start: part.source.text.start, + end: part.source.text.end, + text: part.source.text.value, + } + : undefined, + }, + ] + : [], + ), + agents: nonTextParts.flatMap((part) => + part.type === "agent" + ? [ + { + name: part.name, + source: part.source + ? { start: part.source.start, end: part.source.end, text: part.source.value } + : undefined, + }, + ] + : [], + ), + }, + }) + .then( + () => undefined, + (error) => error, + ) + if (error) { + toast.show({ title: "Failed to send prompt", message: errorMessage(error), variant: "error" }) return false } if (editorParts.length > 0) editor.markSelectionSent() diff --git a/packages/tui/src/component/prompt/move.tsx b/packages/tui/src/component/prompt/move.tsx index 40956a148e..b567fd97f4 100644 --- a/packages/tui/src/component/prompt/move.tsx +++ b/packages/tui/src/component/prompt/move.tsx @@ -37,17 +37,14 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess { projectID, context }, { throwOnError: true }, ) - const result = await sdk.client.v2.projectCopy.create( - { - projectID, - location: { directory: project.instance.directory() || paths.cwd }, - strategy: "git_worktree", - directory: path.join(paths.worktree, projectID.slice(0, 6)), - name: generated.data.name, - }, - { throwOnError: true }, - ) - const directory = result.data?.directory + const result = await sdk.api.projectCopies.create({ + projectID, + location: { directory: project.instance.directory() || paths.cwd }, + strategy: "git_worktree", + directory: path.join(paths.worktree, projectID.slice(0, 6)), + name: generated.data.name, + }) + const directory = result.directory if (!directory) throw new Error("No project copy directory returned") // Call a location-based route to make sure it's bootstrapped diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 3d64e0ba1c..b97e9efb0a 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -678,15 +678,22 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ async function bootstrap() { const settled = await Promise.allSettled([ - sdk.api.sessions.list({ limit: 50, order: "desc" }).then((response) => - setStore( - "session", - "info", - produce((draft) => { - for (const session of response.data) draft[session.id] = mutable(session) - }), + sdk.api.sessions + .list({ + limit: 50, + order: "desc", + directory: defaultLocation().directory, + workspace: defaultLocation().workspaceID, + }) + .then((response) => + setStore( + "session", + "info", + produce((draft) => { + for (const session of response.data) draft[session.id] = mutable(session) + }), + ), ), - ), sdk.api.sessions .active() .then((active) => diff --git a/packages/tui/src/routes/session/dialog-message.tsx b/packages/tui/src/routes/session/dialog-message.tsx index c7c1a7c373..2fa53ef377 100644 --- a/packages/tui/src/routes/session/dialog-message.tsx +++ b/packages/tui/src/routes/session/dialog-message.tsx @@ -11,9 +11,7 @@ export function DialogMessage(props: { messageID: string; sessionID: string; set const clipboard = useClipboard() const toast = useToast() const sdk = useSDK() - const message = createMemo(() => - data.session.message.get(props.sessionID, props.messageID), - ) + const message = createMemo(() => data.session.message.get(props.sessionID, props.messageID)) return ( { - const result = await sdk.client.v2.session.revert.stage({ - sessionID: props.sessionID, - messageID: props.messageID, - }) - if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 }) + await sdk.api.sessions + .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/index.tsx b/packages/tui/src/routes/session/index.tsx index 891d644e6d..a6ef01f26d 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -156,10 +156,11 @@ export function Session() { const promptRef = usePromptRef() const session = createMemo(() => data.session.get(route.sessionID)) const messageIDs = createMemo(() => data.session.message.ids(route.sessionID)) - const sessionMessages = () => messageIDs().flatMap((id) => { - const message = data.session.message.get(route.sessionID, id) - return message ? [message] : [] - }) + const sessionMessages = () => + messageIDs().flatMap((id) => { + const message = data.session.message.get(route.sessionID, id) + return message ? [message] : [] + }) const location = createMemo(() => session()?.location) createEffect(() => { @@ -286,7 +287,10 @@ export function Session() { if (!message) return false if (message.type === "user") return Boolean(message.text.trim()) - return message.type === "assistant" && message.content.some((content) => content.type === "text" && content.text.trim()) + return ( + message.type === "assistant" && + message.content.some((content) => content.type === "text" && content.text.trim()) + ) }) .sort((a, b) => a.y - b.y) @@ -361,7 +365,7 @@ export function Session() { aliases: ["summarize"], }, run: () => { - void sdk.client.v2.session.compact({ sessionID: route.sessionID }) + void sdk.api.sessions.compact({ sessionID: route.sessionID }) dialog.clear() }, }, @@ -395,8 +399,11 @@ export function Session() { dialog.clear() return } - const result = await sdk.client.v2.session.revert.stage({ sessionID: route.sessionID, messageID: target }) - if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 }) + const error = await sdk.api.sessions.stage({ sessionID: route.sessionID, messageID: target }).then( + () => undefined, + (error) => error, + ) + if (error) toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }) dialog.clear() })() }, @@ -409,8 +416,11 @@ export function Session() { slash: { name: "redo" }, run: () => { void (async () => { - const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID }) - if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 }) + const error = await sdk.api.sessions.clear({ sessionID: route.sessionID }).then( + () => undefined, + (error) => error, + ) + if (error) toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }) dialog.clear() })() }, @@ -878,23 +888,21 @@ export function Session() { message.id >= session()!.revert!.messageID && message.type === "user").length} + count={ + messages().filter( + (message) => message.id >= session()!.revert!.messageID && message.type === "user", + ).length + } files={session()!.revert!.files ?? []} /> 0}> - + 0}> - + @@ -957,9 +965,7 @@ function SessionRowView(props: { row: SessionRow; message: (messageID: string) = {(row) => ( - - {(message) => } - + {(message) => } )} @@ -1073,8 +1079,8 @@ function SessionGroupView(props: { result[name] = (result[name] ?? 0) + 1 return result }, {}) - const tools = Object.entries(counts).map(([name, count]) => - `${count} ${count === 1 ? name : name === "search" ? "searches" : `${name}s`}`, + const tools = Object.entries(counts).map( + ([name, count]) => `${count} ${count === 1 ? name : name === "search" ? "searches" : `${name}s`}`, ) return `${props.completed ? "Explored" : "Exploring"} — ${tools.join(", ")}` }) @@ -1116,9 +1122,10 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) { const { theme } = useTheme() const model = createMemo( () => - ctx.models().find( - (model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id, - )?.name ?? `${props.message.model.providerID}/${props.message.model.id}`, + ctx + .models() + .find((model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id) + ?.name ?? `${props.message.model.providerID}/${props.message.model.id}`, ) const duration = createMemo(() => props.message.time.completed ? props.message.time.completed - props.message.time.created : 0, @@ -1158,14 +1165,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessage }) { function CompactionMessage() { const { theme } = useTheme() - return ( - - ) + return } function statusLabel(status: "added" | "modified" | "deleted") { @@ -1197,8 +1197,11 @@ function RevertMessage(props: { onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return void (async () => { - const result = await sdk.client.v2.session.revert.clear({ sessionID: route.sessionID }) - if (result.error) toast.show({ message: errorMessage(result.error), variant: "error", duration: 5000 }) + const error = await sdk.api.sessions.clear({ sessionID: route.sessionID }).then( + () => undefined, + (error) => error, + ) + if (error) toast.show({ message: errorMessage(error), variant: "error", duration: 5000 }) })() }} flexShrink={0} @@ -1207,7 +1210,12 @@ function RevertMessage(props: { customBorderChars={SplitBorder.customBorderChars} borderColor={theme.backgroundPanel} > - + {props.count} message{props.count === 1 ? "" : "s"} reverted @@ -1251,54 +1259,63 @@ function UserMessage(props: { message: SessionMessageUser }) { return ( + { + setHover(true) + }} + onMouseOut={() => { + setHover(false) + }} + onMouseUp={() => { + if (renderer.getSelection()?.getSelectedText()) return + dialog.replace(() => ) + }} + paddingTop={1} + paddingBottom={1} + paddingLeft={2} + backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel} + flexShrink={0} > - { - setHover(true) - }} - onMouseOut={() => { - setHover(false) - }} - onMouseUp={() => { - if (renderer.getSelection()?.getSelectedText()) return - dialog.replace(() => ) - }} - paddingTop={1} - paddingBottom={1} - paddingLeft={2} - backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel} - flexShrink={0} - > - {props.message.text} - - - - {(file) => { - const directory = file.mime === "application/x-directory" - return ( - - - {directory ? " Directory " : " File "} - - {file.name ?? file.uri} - - ) - }} - - - - - - {Locale.todayTimeOrDateTime(props.message.time.created)} - - - + {props.message.text} + + + + {(file) => { + const directory = file.mime === "application/x-directory" + return ( + + + {directory ? " Directory " : " File "} + + + {" "} + {file.name ?? file.uri}{" "} + + + ) + }} + + + + + + {Locale.todayTimeOrDateTime(props.message.time.created)} + + + ) } @@ -1309,9 +1326,10 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole const { theme } = useTheme() const model = createMemo( () => - ctx.models().find( - (model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id, - )?.name ?? `${props.message.model.providerID}/${props.message.model.id}`, + ctx + .models() + .find((model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id) + ?.name ?? `${props.message.model.providerID}/${props.message.model.id}`, ) const final = createMemo(() => { @@ -1325,10 +1343,7 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole }) const exploration = createMemo(() => { - const grouped = new Map< - string, - { first: boolean; parts: SessionMessageAssistantTool[]; active: boolean } - >() + const grouped = new Map() if (!ctx.groupExploration()) return grouped const runs = props.message.content .map((part) => @@ -1471,7 +1486,9 @@ function ReasoningPart(props: { // OpenRouter encrypts some reasoning blocks; drop the placeholder. return props.part.text.replace("[REDACTED]", "").trim() }) - const isDone = createMemo(() => props.part.time?.completed !== undefined || props.message.time.completed !== undefined) + const isDone = createMemo( + () => props.part.time?.completed !== undefined || props.message.time.completed !== undefined, + ) const inMinimal = createMemo(() => ctx.thinkingMode() === "hide") const duration = createMemo(() => { const end = props.part.time?.completed ?? props.message.time.completed @@ -1488,11 +1505,7 @@ function ReasoningPart(props: { return ( - + void }) { return ( - + @@ -2003,12 +2011,7 @@ function Write(props: ToolProps) { - + Write {pathFormatter.format(stringValue(props.input.path))} @@ -2470,7 +2473,14 @@ export function parseApplyPatchFiles(value: unknown) { const patch = stringValue(file.patch) const additions = numberValue(file.additions) const deletions = numberValue(file.deletions) - if (!type || !relativePath || !filePath || patch === undefined || additions === undefined || deletions === undefined) + if ( + !type || + !relativePath || + !filePath || + patch === undefined || + additions === undefined || + deletions === undefined + ) return [] return [{ type, relativePath, filePath, patch, additions, deletions, movePath: stringValue(file.movePath) }] }) diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index 159691a6d1..91e62cd55b 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -186,7 +186,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director onSelect={(option) => { setStore("stage", "permission") if (option === "cancel") return - void sdk.client.v2.session.permission.reply({ + void sdk.api.permissions.reply({ sessionID: props.request.sessionID, reply: "always", requestID: props.request.id, @@ -197,7 +197,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director { - void sdk.client.v2.session.permission.reply({ + void sdk.api.permissions.reply({ sessionID: props.request.sessionID, reply: "reject", requestID: props.request.id, @@ -438,14 +438,14 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director setStore("stage", "reject") return } - void sdk.client.v2.session.permission.reply({ + void sdk.api.permissions.reply({ sessionID: props.request.sessionID, reply: "reject", requestID: props.request.id, }) return } - void sdk.client.v2.session.permission.reply({ + void sdk.api.permissions.reply({ sessionID: props.request.sessionID, reply: "once", requestID: props.request.id, diff --git a/packages/tui/src/routes/session/question.tsx b/packages/tui/src/routes/session/question.tsx index ded2c6db4f..9bbabaaaee 100644 --- a/packages/tui/src/routes/session/question.tsx +++ b/packages/tui/src/routes/session/question.tsx @@ -47,15 +47,15 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?: function submit() { const answers = questions().map((_, i) => store.answers[i] ?? []) - void sdk.client.v2.session.question.reply({ + void sdk.api.questions.reply({ sessionID: props.request.sessionID, requestID: props.request.id, - questionV2Reply: { answers }, + answers, }) } function reject() { - void sdk.client.v2.session.question.reject({ + void sdk.api.questions.reject({ sessionID: props.request.sessionID, requestID: props.request.id, }) @@ -71,10 +71,10 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?: setStore("custom", inputs) } if (single()) { - void sdk.client.v2.session.question.reply({ + void sdk.api.questions.reply({ sessionID: props.request.sessionID, requestID: props.request.id, - questionV2Reply: { answers: [[answer]] }, + answers: [[answer]], }) return }