summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorBrendan Allan <[email protected]>2026-04-17 11:50:17 +0800
committerBrendan Allan <[email protected]>2026-04-17 11:53:17 +0800
commita4882290aa0a0f12c6ba849ce23cda4991017337 (patch)
treeab39db4c19bb4809396563fb89c898d84e25ddf8 /packages/app/src
parent42771c1db377d190b670ec623a951e2ad7d51c3d (diff)
parentfbbab9d6c8a03c4cd5bed0d13a85f52e3aca47ce (diff)
downloadopencode-a4882290aa0a0f12c6ba849ce23cda4991017337.tar.gz
opencode-a4882290aa0a0f12c6ba849ce23cda4991017337.zip
Merge branch 'dev' into nxl/improve-compaction-strategy
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/components/session/session-header.tsx105
-rw-r--r--packages/app/src/components/settings-general.tsx74
-rw-r--r--packages/app/src/components/titlebar.tsx8
-rw-r--r--packages/app/src/context/settings.tsx30
-rw-r--r--packages/app/src/env.d.ts6
-rw-r--r--packages/app/src/i18n/en.ts11
-rw-r--r--packages/app/src/pages/session.tsx178
-rw-r--r--packages/app/src/pages/session/session-side-panel.tsx176
-rw-r--r--packages/app/src/pages/session/use-session-commands.tsx21
9 files changed, 335 insertions, 274 deletions
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<Partial<Record<OpenApp, boolean>>>({
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<HTMLElement | null>(null)
+ const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null)
+ onMount(() => {
+ setCenterMount(document.getElementById("opencode-titlebar-center"))
+ setRightMount(document.getElementById("opencode-titlebar-right"))
+ })
return (
<>
- <Show when={centerMount()}>
+ <Show when={search() && centerMount()}>
{(mount) => (
<Portal mount={mount()}>
<Button
@@ -415,24 +426,28 @@ export function SessionHeader() {
</div>
</Show>
<div class="flex items-center gap-1">
- <Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
- <StatusPopover />
- </Tooltip>
- <TooltipKeybind
- title={language.t("command.terminal.toggle")}
- keybind={command.keybind("terminal.toggle")}
- >
- <Button
- variant="ghost"
- class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
- onClick={toggleTerminal}
- aria-label={language.t("command.terminal.toggle")}
- aria-expanded={view().terminal.opened()}
- aria-controls="terminal-panel"
+ <Show when={status()}>
+ <Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
+ <StatusPopover />
+ </Tooltip>
+ </Show>
+ <Show when={term()}>
+ <TooltipKeybind
+ title={language.t("command.terminal.toggle")}
+ keybind={command.keybind("terminal.toggle")}
>
- <Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
- </Button>
- </TooltipKeybind>
+ <Button
+ variant="ghost"
+ class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
+ onClick={toggleTerminal}
+ aria-label={language.t("command.terminal.toggle")}
+ aria-expanded={view().terminal.opened()}
+ aria-controls="terminal-panel"
+ >
+ <Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
+ </Button>
+ </TooltipKeybind>
+ </Show>
<div class="hidden md:flex items-center gap-1 shrink-0">
<TooltipKeybind
@@ -451,30 +466,32 @@ export function SessionHeader() {
</Button>
</TooltipKeybind>
- <TooltipKeybind
- title={language.t("command.fileTree.toggle")}
- keybind={command.keybind("fileTree.toggle")}
- >
- <Button
- variant="ghost"
- class="titlebar-icon w-8 h-6 p-0 box-border"
- onClick={() => layout.fileTree.toggle()}
- aria-label={language.t("command.fileTree.toggle")}
- aria-expanded={layout.fileTree.opened()}
- aria-controls="file-tree-panel"
+ <Show when={tree()}>
+ <TooltipKeybind
+ title={language.t("command.fileTree.toggle")}
+ keybind={command.keybind("fileTree.toggle")}
>
- <div class="relative flex items-center justify-center size-4">
- <Icon
- size="small"
- name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
- classList={{
- "text-icon-strong": layout.fileTree.opened(),
- "text-icon-weak": !layout.fileTree.opened(),
- }}
- />
- </div>
- </Button>
- </TooltipKeybind>
+ <Button
+ variant="ghost"
+ class="titlebar-icon w-8 h-6 p-0 box-border"
+ onClick={() => layout.fileTree.toggle()}
+ aria-label={language.t("command.fileTree.toggle")}
+ aria-expanded={layout.fileTree.opened()}
+ aria-controls="file-tree-panel"
+ >
+ <div class="relative flex items-center justify-center size-4">
+ <Icon
+ size="small"
+ name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
+ classList={{
+ "text-icon-strong": layout.fileTree.opened(),
+ "text-icon-weak": !layout.fileTree.opened(),
+ }}
+ />
+ </div>
+ </Button>
+ </TooltipKeybind>
+ </Show>
</div>
</div>
</div>
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 = () => {
</div>
)
+ const AdvancedSection = () => (
+ <div class="flex flex-col gap-1">
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
+
+ <SettingsList>
+ <SettingsRow
+ title={language.t("settings.general.row.showFileTree.title")}
+ description={language.t("settings.general.row.showFileTree.description")}
+ >
+ <div data-action="settings-show-file-tree">
+ <Switch
+ checked={settings.general.showFileTree()}
+ onChange={(checked) => settings.general.setShowFileTree(checked)}
+ />
+ </div>
+ </SettingsRow>
+
+ <SettingsRow
+ title={language.t("settings.general.row.showNavigation.title")}
+ description={language.t("settings.general.row.showNavigation.description")}
+ >
+ <div data-action="settings-show-navigation">
+ <Switch
+ checked={settings.general.showNavigation()}
+ onChange={(checked) => settings.general.setShowNavigation(checked)}
+ />
+ </div>
+ </SettingsRow>
+
+ <SettingsRow
+ title={language.t("settings.general.row.showSearch.title")}
+ description={language.t("settings.general.row.showSearch.description")}
+ >
+ <div data-action="settings-show-search">
+ <Switch
+ checked={settings.general.showSearch()}
+ onChange={(checked) => settings.general.setShowSearch(checked)}
+ />
+ </div>
+ </SettingsRow>
+
+ <SettingsRow
+ title={language.t("settings.general.row.showTerminal.title")}
+ description={language.t("settings.general.row.showTerminal.description")}
+ >
+ <div data-action="settings-show-terminal">
+ <Switch
+ checked={settings.general.showTerminal()}
+ onChange={(checked) => settings.general.setShowTerminal(checked)}
+ />
+ </div>
+ </SettingsRow>
+
+ <SettingsRow
+ title={language.t("settings.general.row.showStatus.title")}
+ description={language.t("settings.general.row.showStatus.description")}
+ >
+ <div data-action="settings-show-status">
+ <Switch
+ checked={settings.general.showStatus()}
+ onChange={(checked) => settings.general.setShowStatus(checked)}
+ />
+ </div>
+ </SettingsRow>
+ </SettingsList>
+ </div>
+ )
+
const AppearanceSection = () => (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
@@ -527,6 +596,7 @@ export const SettingsGeneral: Component = () => {
</div>
)
+ console.log(import.meta.env)
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
@@ -609,6 +679,10 @@ export const SettingsGeneral: Component = () => {
)
}}
</Show>
+
+ <Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
+ <AdvancedSection />
+ </Show>
</div>
</div>
)
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() {
<div
class="flex items-center shrink-0"
classList={{
- "translate-x-0": !layout.sidebar.opened(),
- "-translate-x-[36px]": layout.sidebar.opened(),
+ "-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
"duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(),
}}
>
- <Show when={hasProjects()}>
+ <Show when={hasProjects() && nav()}>
<div class="flex items-center gap-0 transition-transform">
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
index afd03365e..a585789ce 100644
--- a/packages/app/src/context/settings.tsx
+++ b/packages/app/src/context/settings.tsx
@@ -23,6 +23,11 @@ export interface Settings {
autoSave: boolean
releaseNotes: boolean
followup: "queue" | "steer"
+ showFileTree: boolean
+ showNavigation: boolean
+ showSearch: boolean
+ showStatus: boolean
+ showTerminal: boolean
showReasoningSummaries: boolean
shellToolPartsExpanded: boolean
editToolPartsExpanded: boolean
@@ -89,6 +94,11 @@ const defaultSettings: Settings = {
autoSave: true,
releaseNotes: true,
followup: "steer",
+ showFileTree: false,
+ showNavigation: false,
+ showSearch: false,
+ showStatus: false,
+ showTerminal: false,
showReasoningSummaries: false,
shellToolPartsExpanded: false,
editToolPartsExpanded: false,
@@ -162,6 +172,26 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setFollowup(value: "queue" | "steer") {
setStore("general", "followup", value === "queue" ? "steer" : value)
},
+ showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree),
+ setShowFileTree(value: boolean) {
+ setStore("general", "showFileTree", value)
+ },
+ showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation),
+ setShowNavigation(value: boolean) {
+ setStore("general", "showNavigation", value)
+ },
+ showSearch: withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch),
+ setShowSearch(value: boolean) {
+ setStore("general", "showSearch", value)
+ },
+ showStatus: withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus),
+ setShowStatus(value: boolean) {
+ setStore("general", "showStatus", value)
+ },
+ showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal),
+ setShowTerminal(value: boolean) {
+ setStore("general", "showTerminal", value)
+ },
showReasoningSummaries: withFallback(
() => store.general?.showReasoningSummaries,
defaultSettings.general.showReasoningSummaries,
diff --git a/packages/app/src/env.d.ts b/packages/app/src/env.d.ts
index 22e52f991..9b03d336f 100644
--- a/packages/app/src/env.d.ts
+++ b/packages/app/src/env.d.ts
@@ -1,16 +1,14 @@
-import "solid-js"
-
interface ImportMetaEnv {
readonly VITE_OPENCODE_SERVER_HOST: string
readonly VITE_OPENCODE_SERVER_PORT: string
- readonly OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
+ readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
-declare module "solid-js" {
+export declare module "solid-js" {
namespace JSX {
interface Directives {
sortable: true
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index c6bcc37b1..2cfb79714 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -719,6 +719,7 @@ export const dict = {
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
"settings.general.section.appearance": "Appearance",
+ "settings.general.section.advanced": "Advanced",
"settings.general.section.notifications": "System notifications",
"settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Sound effects",
@@ -741,6 +742,16 @@ export const dict = {
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
"settings.general.row.followup.option.queue": "Queue",
"settings.general.row.followup.option.steer": "Steer",
+ "settings.general.row.showFileTree.title": "File tree",
+ "settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions",
+ "settings.general.row.showNavigation.title": "Navigation controls",
+ "settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
+ "settings.general.row.showSearch.title": "Command palette",
+ "settings.general.row.showSearch.description": "Show the search and command palette button in the desktop title bar",
+ "settings.general.row.showTerminal.title": "Terminal",
+ "settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
+ "settings.general.row.showStatus.title": "Server status",
+ "settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
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"),