From f135c0b5eefae50e95f549002aea4bd1e510c4f7 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 17 Apr 2026 10:27:08 +0800 Subject: app: use tanstack query to load session vcs state (#22277) --- packages/app/src/pages/session.tsx | 178 ++++++++++--------------------------- 1 file changed, 45 insertions(+), 133 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index c4d642bf8..4ae973b85 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,6 +1,6 @@ -import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { Project, UserMessage } from "@opencode-ai/sdk/v2" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useMutation } from "@tanstack/solid-query" +import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query" import { batch, onCleanup, @@ -324,6 +324,7 @@ export default function Page() { const local = useLocal() const file = useFile() const sync = useSync() + const queryClient = useQueryClient() const dialog = useDialog() const language = useLanguage() const sdk = useSDK() @@ -518,26 +519,6 @@ export default function Page() { deferRender: false, }) - const [vcs, setVcs] = createStore<{ - diff: { - git: VcsFileDiff[] - branch: VcsFileDiff[] - } - ready: { - git: boolean - branch: boolean - } - }>({ - diff: { - git: [] as VcsFileDiff[], - branch: [] as VcsFileDiff[], - }, - ready: { - git: false, - branch: false, - }, - }) - const [followup, setFollowup] = persisted( Persist.workspace(sdk.directory, "followup", ["followup.v1"]), createStore<{ @@ -571,68 +552,6 @@ export default function Page() { let todoTimer: number | undefined let diffFrame: number | undefined let diffTimer: number | undefined - const vcsTask = new Map>() - const vcsRun = new Map() - - const bumpVcs = (mode: VcsMode) => { - const next = (vcsRun.get(mode) ?? 0) + 1 - vcsRun.set(mode, next) - return next - } - - const resetVcs = (mode?: VcsMode) => { - const list = mode ? [mode] : (["git", "branch"] as const) - list.forEach((item) => { - bumpVcs(item) - vcsTask.delete(item) - setVcs("diff", item, []) - setVcs("ready", item, false) - }) - } - - const loadVcs = (mode: VcsMode, force = false) => { - if (sync.project?.vcs !== "git") return Promise.resolve() - if (!force && vcs.ready[mode]) return Promise.resolve() - - if (force) { - if (vcsTask.has(mode)) bumpVcs(mode) - vcsTask.delete(mode) - setVcs("ready", mode, false) - } - - const current = vcsTask.get(mode) - if (current) return current - - const run = bumpVcs(mode) - - const task = sdk.client.vcs - .diff({ mode }) - .then((result) => { - if (vcsRun.get(mode) !== run) return - setVcs("diff", mode, list(result.data)) - setVcs("ready", mode, true) - }) - .catch((error) => { - if (vcsRun.get(mode) !== run) return - console.debug("[session-review] failed to load vcs diff", { mode, error }) - setVcs("diff", mode, []) - setVcs("ready", mode, true) - }) - .finally(() => { - if (vcsTask.get(mode) === task) vcsTask.delete(mode) - }) - - vcsTask.set(mode, task) - return task - } - - const refreshVcs = () => { - resetVcs() - const mode = untrack(vcsMode) - if (!mode) return - if (!untrack(wantsReview)) return - void loadVcs(mode, true) - } createComputed((prev) => { const open = desktopReviewOpen() @@ -663,21 +582,52 @@ export default function Page() { list.push("turn") return list }) + const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") + const wantsReview = createMemo(() => + isDesktop() + ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") + : store.mobileTab === "changes", + ) const vcsMode = createMemo(() => { if (store.changes === "git" || store.changes === "branch") return store.changes }) - const reviewDiffs = createMemo(() => { - if (store.changes === "git") return list(vcs.diff.git) - if (store.changes === "branch") return list(vcs.diff.branch) - return turnDiffs() + const vcsKey = createMemo( + () => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const, + ) + const vcsQuery = createQuery(() => { + const mode = vcsMode() + const enabled = wantsReview() && sync.project?.vcs === "git" + + return { + queryKey: [...vcsKey(), mode] as const, + enabled, + staleTime: Number.POSITIVE_INFINITY, + gcTime: 60 * 1000, + queryFn: mode + ? () => + sdk.client.vcs + .diff({ mode }) + .then((result) => list(result.data)) + .catch((error) => { + console.debug("[session-review] failed to load vcs diff", { mode, error }) + return [] + }) + : skipToken, + } }) - const reviewCount = createMemo(() => reviewDiffs().length) - const hasReview = createMemo(() => reviewCount() > 0) - const reviewReady = createMemo(() => { - if (store.changes === "git") return vcs.ready.git - if (store.changes === "branch") return vcs.ready.branch + const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() }) + const reviewDiffs = () => { + if (store.changes === "git" || store.changes === "branch") + // avoids suspense + return vcsQuery.isFetched ? (vcsQuery.data ?? []) : [] + return turnDiffs() + } + const reviewCount = () => reviewDiffs().length + const hasReview = () => reviewCount() > 0 + const reviewReady = () => { + if (store.changes === "git" || store.changes === "branch") return !vcsQuery.isPending return true - }) + } const newSessionWorktree = createMemo(() => { if (store.newSessionWorktree === "create") return "create" @@ -897,27 +847,6 @@ export default function Page() { ), ) - createEffect( - on( - () => sdk.directory, - () => { - resetVcs() - }, - { defer: true }, - ), - ) - - createEffect( - on( - () => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const, - (next, prev) => { - if (prev === undefined || same(next, prev)) return - refreshVcs() - }, - { defer: true }, - ), - ) - const stopVcs = sdk.event.listen((evt) => { if (evt.details.type !== "file.watcher.updated") return const props = @@ -1051,13 +980,6 @@ export default function Page() { } } - const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") - const wantsReview = createMemo(() => - isDesktop() - ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") - : store.mobileTab === "changes", - ) - createEffect(() => { const list = changesOptions() if (list.includes(store.changes)) return @@ -1066,22 +988,12 @@ export default function Page() { setStore("changes", next) }) - createEffect(() => { - const mode = vcsMode() - if (!mode) return - if (!wantsReview()) return - void loadVcs(mode) - }) - createEffect( on( () => sync.data.session_status[params.id ?? ""]?.type, (next, prev) => { - const mode = vcsMode() - if (!mode) return - if (!wantsReview()) return if (next !== "idle" || prev === undefined || prev === "idle") return - void loadVcs(mode, true) + refreshVcs() }, { defer: true }, ), -- cgit v1.2.3 From fbbab9d6c8a03c4cd5bed0d13a85f52e3aca47ce Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 16 Apr 2026 23:31:00 -0400 Subject: feat(app): hide desktop titlebar tools behind settings (#19029) Co-authored-by: Brendan Allan Co-authored-by: Brendan Allan --- .../app/src/components/session/session-header.tsx | 105 ++++++------ packages/app/src/components/settings-general.tsx | 74 +++++++++ packages/app/src/components/titlebar.tsx | 8 +- packages/app/src/context/settings.tsx | 30 ++++ packages/app/src/env.d.ts | 6 +- packages/app/src/i18n/en.ts | 11 ++ .../app/src/pages/session/session-side-panel.tsx | 176 +++++++++++---------- .../app/src/pages/session/use-session-commands.tsx | 21 ++- 8 files changed, 290 insertions(+), 141 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 7acfdfc37..021e5be67 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -8,7 +8,7 @@ import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/shared/util/path" -import { createEffect, createMemo, For, Show } from "solid-js" +import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { useCommand } from "@/context/command" @@ -16,6 +16,7 @@ import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useServer } from "@/context/server" +import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { focusTerminalById } from "@/pages/session/helpers" @@ -134,6 +135,7 @@ export function SessionHeader() { const server = useServer() const platform = usePlatform() const language = useLanguage() + const settings = useSettings() const sync = useSync() const terminal = useTerminal() const { params, view } = useSessionLayout() @@ -151,6 +153,11 @@ export function SessionHeader() { }) const hotkey = createMemo(() => command.keybind("file.open")) const os = createMemo(() => detectOS(platform)) + const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta" + const search = createMemo(() => !isDesktopBeta || settings.general.showSearch()) + const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree()) + const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal()) + const status = createMemo(() => !isDesktopBeta || settings.general.showStatus()) const [exists, setExists] = createStore>>({ finder: true, @@ -262,12 +269,16 @@ export function SessionHeader() { .catch((err: unknown) => showRequestError(language, err)) } - const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center")) - const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right")) + const [centerMount, setCenterMount] = createSignal(null) + const [rightMount, setRightMount] = createSignal(null) + onMount(() => { + setCenterMount(document.getElementById("opencode-titlebar-center")) + setRightMount(document.getElementById("opencode-titlebar-right")) + }) return ( <> - + {(mount) => ( - + + + diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index b4ac061df..c380fb69b 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -106,6 +106,7 @@ export const SettingsGeneral: Component = () => { permission.disableAutoAccept(params.id, value) } + const desktop = createMemo(() => platform.platform === "desktop") const check = () => { if (!platform.checkUpdate) return @@ -279,6 +280,74 @@ export const SettingsGeneral: Component = () => { ) + const AdvancedSection = () => ( +
+

{language.t("settings.general.section.advanced")}

+ + + +
+ settings.general.setShowFileTree(checked)} + /> +
+
+ + +
+ settings.general.setShowNavigation(checked)} + /> +
+
+ + +
+ settings.general.setShowSearch(checked)} + /> +
+
+ + +
+ settings.general.setShowTerminal(checked)} + /> +
+
+ + +
+ settings.general.setShowStatus(checked)} + /> +
+
+
+
+ ) + const AppearanceSection = () => (

{language.t("settings.general.section.appearance")}

@@ -527,6 +596,7 @@ export const SettingsGeneral: Component = () => {
) + console.log(import.meta.env) return (
@@ -609,6 +679,10 @@ export const SettingsGeneral: Component = () => { ) }} + + + +
) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index b7edb85ed..409fcbeff 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -11,6 +11,7 @@ import { useLayout } from "@/context/layout" import { usePlatform } from "@/context/platform" import { useCommand } from "@/context/command" import { useLanguage } from "@/context/language" +import { useSettings } from "@/context/settings" import { applyPath, backPath, forwardPath } from "./titlebar-history" type TauriDesktopWindow = { @@ -40,6 +41,7 @@ export function Titlebar() { const platform = usePlatform() const command = useCommand() const language = useLanguage() + const settings = useSettings() const theme = useTheme() const navigate = useNavigate() const location = useLocation() @@ -78,6 +80,7 @@ export function Titlebar() { const canBack = createMemo(() => history.index > 0) const canForward = createMemo(() => history.index < history.stack.length - 1) const hasProjects = createMemo(() => layout.projects.list().length > 0) + const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation()) const back = () => { const next = backPath(history) @@ -255,13 +258,12 @@ export function Titlebar() {
- +
-
+
- - - - {props.reviewCount()}{" "} - {language.t( - props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", - )} - - - {language.t("session.files.all")} - - - - - - - {language.t("common.loading")} - {language.t("common.loading.ellipsis")} -
- } - > + + + + {props.reviewCount()}{" "} + {language.t( + props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", + )} + + + {language.t("session.files.all")} + + + + + + + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+ } + > + props.focusReviewDiff(node.path)} + /> +
+ + + + + + {empty(language.t("session.files.empty"))} + props.focusReviewDiff(node.path)} + onFileClick={(node) => openTab(file.tab(node.path))} /> - - - {empty(props.empty())} - - - - - {empty(language.t("session.files.empty"))} - - openTab(file.tab(node.path))} - /> - - - - - - -
props.size.start()}> - { - props.size.touch() - layout.fileTree.resize(width) - }} - /> + + + +
-
- + +
props.size.start()}> + { + props.size.touch() + layout.fileTree.resize(width) + }} + /> +
+
+ + diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index b5d254463..9bbeb10bd 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -7,8 +7,10 @@ import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useLocal } from "@/context/local" import { usePermission } from "@/context/permission" +import { usePlatform } from "@/context/platform" import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" +import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { showToast } from "@opencode-ai/ui/toast" @@ -39,8 +41,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const language = useLanguage() const local = useLocal() const permission = usePermission() + const platform = usePlatform() const prompt = usePrompt() const sdk = useSDK() + const settings = useSettings() const sync = useSync() const terminal = useTerminal() const layout = useLayout() @@ -66,6 +70,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { }) const activeFileTab = tabState.activeFileTab const closableTab = tabState.closableTab + const shown = () => platform.platform !== "desktop" || settings.general.showFileTree() const idle = { type: "idle" as const } const status = () => sync.data.session_status[params.id ?? ""] ?? idle @@ -457,12 +462,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => { keybind: "mod+shift+r", onSelect: () => view().reviewPanel.toggle(), }), - viewCommand({ - id: "fileTree.toggle", - title: language.t("command.fileTree.toggle"), - keybind: "mod+\\", - onSelect: () => layout.fileTree.toggle(), - }), + ...(shown() + ? [ + viewCommand({ + id: "fileTree.toggle", + title: language.t("command.fileTree.toggle"), + keybind: "mod+\\", + onSelect: () => layout.fileTree.toggle(), + }), + ] + : []), viewCommand({ id: "input.focus", title: language.t("command.input.focus"), -- cgit v1.2.3