summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages/session
diff options
context:
space:
mode:
authorBrendan Allan <[email protected]>2026-03-21 23:33:04 +0800
committerGitHub <[email protected]>2026-03-21 23:33:04 +0800
commit6a16db4b929422b6f5ef7072ac889cec41ae1eb2 (patch)
tree2663160c18632706372f83432d44f1a741a613bf /packages/app/src/pages/session
parent9ad6588f3e0066125033810a5e0e4dc08f0c6961 (diff)
downloadopencode-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.tsx84
-rw-r--r--packages/app/src/pages/session/message-timeline.tsx124
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>