diff options
| author | Brendan Allan <[email protected]> | 2026-03-21 23:33:04 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-21 23:33:04 +0800 |
| commit | 6a16db4b929422b6f5ef7072ac889cec41ae1eb2 (patch) | |
| tree | 2663160c18632706372f83432d44f1a741a613bf /packages/app/src/pages/session | |
| parent | 9ad6588f3e0066125033810a5e0e4dc08f0c6961 (diff) | |
| download | opencode-6a16db4b929422b6f5ef7072ac889cec41ae1eb2.tar.gz opencode-6a16db4b929422b6f5ef7072ac889cec41ae1eb2.zip | |
app: manage mutation loading states with tanstack query (#18501)
Diffstat (limited to 'packages/app/src/pages/session')
| -rw-r--r-- | packages/app/src/pages/session/composer/session-question-dock.tsx | 84 | ||||
| -rw-r--r-- | packages/app/src/pages/session/message-timeline.tsx | 124 |
2 files changed, 104 insertions, 104 deletions
diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index b66c27579..7ba07b15d 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -1,5 +1,6 @@ import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js" import { createStore } from "solid-js/store" +import { useMutation } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" import { DockPrompt } from "@opencode-ai/ui/dock-prompt" import { Icon } from "@opencode-ai/ui/icon" @@ -24,7 +25,6 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit custom: cached?.custom ?? ([] as string[]), customOn: cached?.customOn ?? ([] as boolean[]), editing: false, - sending: false, }) let root: HTMLDivElement | undefined @@ -126,36 +126,40 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit showToast({ title: language.t("common.requestFailed"), description: message }) } - const reply = async (answers: QuestionAnswer[]) => { - if (store.sending) return - - props.onSubmit() - setStore("sending", true) - try { - await sdk.client.question.reply({ requestID: props.request.id, answers }) + const replyMutation = useMutation(() => ({ + mutationFn: (answers: QuestionAnswer[]) => sdk.client.question.reply({ requestID: props.request.id, answers }), + onMutate: () => { + props.onSubmit() + }, + onSuccess: () => { replied = true cache.delete(props.request.id) - } catch (err) { - fail(err) - } finally { - setStore("sending", false) - } + }, + onError: fail, + })) + + const rejectMutation = useMutation(() => ({ + mutationFn: () => sdk.client.question.reject({ requestID: props.request.id }), + onMutate: () => { + props.onSubmit() + }, + onSuccess: () => { + replied = true + cache.delete(props.request.id) + }, + onError: fail, + })) + + const sending = createMemo(() => replyMutation.isPending || rejectMutation.isPending) + + const reply = async (answers: QuestionAnswer[]) => { + if (sending()) return + await replyMutation.mutateAsync(answers) } const reject = async () => { - if (store.sending) return - - props.onSubmit() - setStore("sending", true) - try { - await sdk.client.question.reject({ requestID: props.request.id }) - replied = true - cache.delete(props.request.id) - } catch (err) { - fail(err) - } finally { - setStore("sending", false) - } + if (sending()) return + await rejectMutation.mutateAsync() } const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? [])) @@ -175,7 +179,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } const customToggle = () => { - if (store.sending) return + if (sending()) return if (!multi()) { setStore("customOn", store.tab, true) @@ -198,14 +202,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } const customOpen = () => { - if (store.sending) return + if (sending()) return if (!on()) setStore("customOn", store.tab, true) setStore("editing", true) customUpdate(input(), true) } const selectOption = (optIndex: number) => { - if (store.sending) return + if (sending()) return if (optIndex === options().length) { customOpen() @@ -227,7 +231,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } const next = () => { - if (store.sending) return + if (sending()) return if (store.editing) commitCustom() if (store.tab >= total() - 1) { @@ -240,14 +244,14 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } const back = () => { - if (store.sending) return + if (sending()) return if (store.tab <= 0) return setStore("tab", store.tab - 1) setStore("editing", false) } const jump = (tab: number) => { - if (store.sending) return + if (sending()) return setStore("tab", tab) setStore("editing", false) } @@ -270,7 +274,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit (store.answers[i()]?.length ?? 0) > 0 || (store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0) } - disabled={store.sending} + disabled={sending()} onClick={() => jump(i())} aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`} /> @@ -281,16 +285,16 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } footer={ <> - <Button variant="ghost" size="large" disabled={store.sending} onClick={reject}> + <Button variant="ghost" size="large" disabled={sending()} onClick={reject}> {language.t("ui.common.dismiss")} </Button> <div data-slot="question-footer-actions"> <Show when={store.tab > 0}> - <Button variant="secondary" size="large" disabled={store.sending} onClick={back}> + <Button variant="secondary" size="large" disabled={sending()} onClick={back}> {language.t("ui.common.back")} </Button> </Show> - <Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}> + <Button variant={last() ? "primary" : "secondary"} size="large" disabled={sending()} onClick={next}> {last() ? language.t("ui.common.submit") : language.t("ui.common.next")} </Button> </div> @@ -311,7 +315,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit data-picked={picked()} role={multi() ? "checkbox" : "radio"} aria-checked={picked()} - disabled={store.sending} + disabled={sending()} onClick={() => selectOption(i())} > <span data-slot="question-option-check" aria-hidden="true"> @@ -345,7 +349,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit data-picked={on()} role={multi() ? "checkbox" : "radio"} aria-checked={on()} - disabled={store.sending} + disabled={sending()} onClick={customOpen} > <span @@ -377,7 +381,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit role={multi() ? "checkbox" : "radio"} aria-checked={on()} onMouseDown={(e) => { - if (store.sending) { + if (sending()) { e.preventDefault() return } @@ -419,7 +423,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit placeholder={language.t("ui.question.custom.placeholder")} value={input()} rows={1} - disabled={store.sending} + disabled={sending()} onKeyDown={(e) => { if (e.key === "Escape") { e.preventDefault() diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 74f2e8c2c..2ec5352aa 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,6 +1,7 @@ import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js" import { createStore, produce } from "solid-js/store" import { useNavigate } from "@solidjs/router" +import { useMutation } from "@tanstack/solid-query" import { Button } from "@opencode-ai/ui/button" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" @@ -321,7 +322,6 @@ export function MessageTimeline(props: { const [title, setTitle] = createStore({ draft: "", editing: false, - saving: false, menuOpen: false, pendingRename: false, pendingShare: false, @@ -335,38 +335,6 @@ export function MessageTimeline(props: { let more: HTMLButtonElement | undefined - const [req, setReq] = createStore({ share: false, unshare: false }) - - const shareSession = () => { - const id = sessionID() - if (!id || req.share) return - if (!shareEnabled()) return - setReq("share", true) - globalSDK.client.session - .share({ sessionID: id, directory: sdk.directory }) - .catch((err: unknown) => { - console.error("Failed to share session", err) - }) - .finally(() => { - setReq("share", false) - }) - } - - const unshareSession = () => { - const id = sessionID() - if (!id || req.unshare) return - if (!shareEnabled()) return - setReq("unshare", true) - globalSDK.client.session - .unshare({ sessionID: id, directory: sdk.directory }) - .catch((err: unknown) => { - console.error("Failed to unshare session", err) - }) - .finally(() => { - setReq("unshare", false) - }) - } - const viewShare = () => { const url = shareUrl() if (!url) return @@ -382,6 +350,53 @@ export function MessageTimeline(props: { return language.t("common.requestFailed") } + const shareMutation = useMutation(() => ({ + mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }), + onError: (err) => { + console.error("Failed to share session", err) + }, + })) + + const unshareMutation = useMutation(() => ({ + mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }), + onError: (err) => { + console.error("Failed to unshare session", err) + }, + })) + + const titleMutation = useMutation(() => ({ + mutationFn: (input: { id: string; title: string }) => sdk.client.session.update({ sessionID: input.id, title: input.title }), + onSuccess: (_, input) => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === input.id) + if (index !== -1) draft.session[index].title = input.title + }), + ) + setTitle("editing", false) + }, + onError: (err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }, + })) + + const shareSession = () => { + const id = sessionID() + if (!id || shareMutation.isPending) return + if (!shareEnabled()) return + shareMutation.mutate(id) + } + + const unshareSession = () => { + const id = sessionID() + if (!id || unshareMutation.isPending) return + if (!shareEnabled()) return + unshareMutation.mutate(id) + } + createEffect( on( sessionKey, @@ -389,7 +404,6 @@ export function MessageTimeline(props: { setTitle({ draft: "", editing: false, - saving: false, menuOpen: false, pendingRename: false, pendingShare: false, @@ -408,40 +422,22 @@ export function MessageTimeline(props: { } const closeTitleEditor = () => { - if (title.saving) return - setTitle({ editing: false, saving: false }) + if (titleMutation.isPending) return + setTitle("editing", false) } - const saveTitleEditor = async () => { + const saveTitleEditor = () => { const id = sessionID() if (!id) return - if (title.saving) return + if (titleMutation.isPending) return const next = title.draft.trim() if (!next || next === (titleValue() ?? "")) { - setTitle({ editing: false, saving: false }) + setTitle("editing", false) return } - setTitle("saving", true) - await sdk.client.session - .update({ sessionID: id, title: next }) - .then(() => { - sync.set( - produce((draft) => { - const index = draft.session.findIndex((s) => s.id === id) - if (index !== -1) draft.session[index].title = next - }), - ) - setTitle({ editing: false, saving: false }) - }) - .catch((err) => { - setTitle("saving", false) - showToast({ - title: language.t("common.requestFailed"), - description: errorMessage(err), - }) - }) + titleMutation.mutate({ id, title: next }) } const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { @@ -712,7 +708,7 @@ export function MessageTimeline(props: { titleRef = el }} value={title.draft} - disabled={title.saving} + disabled={titleMutation.isPending} class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]" style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} onInput={(event) => setTitle("draft", event.currentTarget.value)} @@ -863,9 +859,9 @@ export function MessageTimeline(props: { variant="primary" class="w-full" onClick={shareSession} - disabled={req.share} + disabled={shareMutation.isPending} > - {req.share + {shareMutation.isPending ? language.t("session.share.action.publishing") : language.t("session.share.action.publish")} </Button> @@ -886,9 +882,9 @@ export function MessageTimeline(props: { variant="secondary" class="w-full shadow-none border border-border-weak-base" onClick={unshareSession} - disabled={req.unshare} + disabled={unshareMutation.isPending} > - {req.unshare + {unshareMutation.isPending ? language.t("session.share.action.unpublishing") : language.t("session.share.action.unpublish")} </Button> @@ -897,7 +893,7 @@ export function MessageTimeline(props: { variant="primary" class="w-full" onClick={viewShare} - disabled={req.unshare} + disabled={unshareMutation.isPending} > {language.t("session.share.action.view")} </Button> |
