diff options
| author | Brendan Allan <[email protected]> | 2026-04-17 11:50:17 +0800 |
|---|---|---|
| committer | Brendan Allan <[email protected]> | 2026-04-17 11:53:17 +0800 |
| commit | a4882290aa0a0f12c6ba849ce23cda4991017337 (patch) | |
| tree | ab39db4c19bb4809396563fb89c898d84e25ddf8 /packages/app/src/pages | |
| parent | 42771c1db377d190b670ec623a951e2ad7d51c3d (diff) | |
| parent | fbbab9d6c8a03c4cd5bed0d13a85f52e3aca47ce (diff) | |
| download | opencode-a4882290aa0a0f12c6ba849ce23cda4991017337.tar.gz opencode-a4882290aa0a0f12c6ba849ce23cda4991017337.zip | |
Merge branch 'dev' into nxl/improve-compaction-strategy
Diffstat (limited to 'packages/app/src/pages')
| -rw-r--r-- | packages/app/src/pages/session.tsx | 178 | ||||
| -rw-r--r-- | packages/app/src/pages/session/session-side-panel.tsx | 176 | ||||
| -rw-r--r-- | packages/app/src/pages/session/use-session-commands.tsx | 21 |
3 files changed, 152 insertions, 223 deletions
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<VcsMode, Promise<void>>() - const vcsRun = new Map<VcsMode, number>() - - 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<VcsMode | undefined>(() => { 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 }, ), diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index cddbea84d..06cbec48b 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -19,6 +19,9 @@ import { useCommand } from "@/context/command" import { useFile, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" +import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" +import { useSync } from "@/context/sync" import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { FileTabContent } from "@/pages/session/file-tabs" import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers" @@ -39,6 +42,9 @@ export function SessionSidePanel(props: { size: Sizing }) { const layout = useLayout() + const platform = usePlatform() + const settings = useSettings() + const sync = useSync() const file = useFile() const language = useLanguage() const command = useCommand() @@ -46,9 +52,10 @@ export function SessionSidePanel(props: { const { sessionKey, tabs, view } = useSessionLayout() const isDesktop = createMediaQuery("(min-width: 768px)") + const shown = createMemo(() => platform.platform !== "desktop" || settings.general.showFileTree()) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) - const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) + const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened()) const open = createMemo(() => reviewOpen() || fileOpen()) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { @@ -341,98 +348,99 @@ export function SessionSidePanel(props: { </div> </div> - <div - id="file-tree-panel" - aria-hidden={!fileOpen()} - inert={!fileOpen()} - class="relative min-w-0 h-full shrink-0 overflow-hidden" - classList={{ - "pointer-events-none": !fileOpen(), - "transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none": - !props.size.active(), - }} - style={{ width: treeWidth() }} - > + <Show when={shown()}> <div - class="h-full flex flex-col overflow-hidden group/filetree" - classList={{ "border-l border-border-weaker-base": reviewOpen() }} + id="file-tree-panel" + aria-hidden={!fileOpen()} + inert={!fileOpen()} + class="relative min-w-0 h-full shrink-0 overflow-hidden" + classList={{ + "pointer-events-none": !fileOpen(), + "transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none": + !props.size.active(), + }} + style={{ width: treeWidth() }} > - <Tabs - variant="pill" - value={fileTreeTab()} - onChange={setFileTreeTabValue} - class="h-full" - data-scope="filetree" + <div + class="h-full flex flex-col overflow-hidden group/filetree" + classList={{ "border-l border-border-weaker-base": reviewOpen() }} > - <Tabs.List> - <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}> - {props.reviewCount()}{" "} - {language.t( - props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", - )} - </Tabs.Trigger> - <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}> - {language.t("session.files.all")} - </Tabs.Trigger> - </Tabs.List> - <Tabs.Content value="changes" class="bg-background-stronger px-3 py-0"> - <Switch> - <Match when={props.hasReview() || !props.diffsReady()}> - <Show - when={props.diffsReady()} - fallback={ - <div class="px-2 py-2 text-12-regular text-text-weak"> - {language.t("common.loading")} - {language.t("common.loading.ellipsis")} - </div> - } - > + <Tabs + variant="pill" + value={fileTreeTab()} + onChange={setFileTreeTabValue} + class="h-full" + data-scope="filetree" + > + <Tabs.List> + <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}> + {props.reviewCount()}{" "} + {language.t( + props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other", + )} + </Tabs.Trigger> + <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}> + {language.t("session.files.all")} + </Tabs.Trigger> + </Tabs.List> + <Tabs.Content value="changes" class="bg-background-stronger px-3 py-0"> + <Switch> + <Match when={props.hasReview() || !props.diffsReady()}> + <Show + when={props.diffsReady()} + fallback={ + <div class="px-2 py-2 text-12-regular text-text-weak"> + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} + </div> + } + > + <FileTree + path="" + class="pt-3" + allowed={diffFiles()} + kinds={kinds()} + draggable={false} + active={props.activeDiff} + onFileClick={(node) => props.focusReviewDiff(node.path)} + /> + </Show> + </Match> + </Switch> + </Tabs.Content> + <Tabs.Content value="all" class="bg-background-stronger px-3 py-0"> + <Switch> + <Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match> + <Match when={true}> <FileTree path="" class="pt-3" - allowed={diffFiles()} + modified={diffFiles()} kinds={kinds()} - draggable={false} - active={props.activeDiff} - onFileClick={(node) => props.focusReviewDiff(node.path)} + onFileClick={(node) => openTab(file.tab(node.path))} /> - </Show> - </Match> - <Match when={true}>{empty(props.empty())}</Match> - </Switch> - </Tabs.Content> - <Tabs.Content value="all" class="bg-background-stronger px-3 py-0"> - <Switch> - <Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match> - <Match when={true}> - <FileTree - path="" - class="pt-3" - modified={diffFiles()} - kinds={kinds()} - onFileClick={(node) => openTab(file.tab(node.path))} - /> - </Match> - </Switch> - </Tabs.Content> - </Tabs> - </div> - <Show when={fileOpen()}> - <div onPointerDown={() => props.size.start()}> - <ResizeHandle - direction="horizontal" - edge="start" - size={layout.fileTree.width()} - min={200} - max={480} - onResize={(width) => { - props.size.touch() - layout.fileTree.resize(width) - }} - /> + </Match> + </Switch> + </Tabs.Content> + </Tabs> </div> - </Show> - </div> + <Show when={fileOpen()}> + <div onPointerDown={() => props.size.start()}> + <ResizeHandle + direction="horizontal" + edge="start" + size={layout.fileTree.width()} + min={200} + max={480} + onResize={(width) => { + props.size.touch() + layout.fileTree.resize(width) + }} + /> + </div> + </Show> + </div> + </Show> </div> </aside> </Show> 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"), |
