summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorFilip <[email protected]>2026-02-25 07:39:58 +0100
committerGitHub <[email protected]>2026-02-25 14:39:58 +0800
commitfc6e7934bd365ad1665dea68556dbfc80ac3b611 (patch)
treea79e707c7e2744d4adbaea16811defadc50e65a1 /packages/app/src/components
parentd7500b25b8eb84051e13bd930e234445352cc9a4 (diff)
downloadopencode-fc6e7934bd365ad1665dea68556dbfc80ac3b611.tar.gz
opencode-fc6e7934bd365ad1665dea68556dbfc80ac3b611.zip
feat(desktop): enhance Windows app resolution and UI loading states (#13320)
Co-authored-by: Brendan Allan <[email protected]> Co-authored-by: Brendan Allan <[email protected]>
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/session/session-header.tsx127
1 files changed, 98 insertions, 29 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index 825d1dab6..d531fa50a 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -1,28 +1,28 @@
+import { AppIcon } from "@opencode-ai/ui/app-icon"
+import { Button } from "@opencode-ai/ui/button"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Icon } from "@opencode-ai/ui/icon"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { Keybind } from "@opencode-ai/ui/keybind"
+import { Popover } from "@opencode-ai/ui/popover"
+import { Spinner } from "@opencode-ai/ui/spinner"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { showToast } from "@opencode-ai/ui/toast"
+import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { getFilename } from "@opencode-ai/util/path"
+import { useParams } from "@solidjs/router"
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
-import { useParams } from "@solidjs/router"
-import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command"
+import { useGlobalSDK } from "@/context/global-sdk"
import { useLanguage } from "@/context/language"
+import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { useSync } from "@/context/sync"
-import { useGlobalSDK } from "@/context/global-sdk"
-import { getFilename } from "@opencode-ai/util/path"
import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist"
-
-import { Icon } from "@opencode-ai/ui/icon"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { Button } from "@opencode-ai/ui/button"
-import { AppIcon } from "@opencode-ai/ui/app-icon"
-import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
-import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
-import { Popover } from "@opencode-ai/ui/popover"
-import { TextField } from "@opencode-ai/ui/text-field"
-import { Keybind } from "@opencode-ai/ui/keybind"
-import { showToast } from "@opencode-ai/ui/toast"
import { StatusPopover } from "../status-popover"
const OPEN_APPS = [
@@ -45,32 +45,67 @@ type OpenApp = (typeof OPEN_APPS)[number]
type OS = "macos" | "windows" | "linux" | "unknown"
const MAC_APPS = [
- { id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
+ {
+ id: "vscode",
+ label: "VS Code",
+ icon: "vscode",
+ openWith: "Visual Studio Code",
+ },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
- { id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
+ {
+ id: "antigravity",
+ label: "Antigravity",
+ icon: "antigravity",
+ openWith: "Antigravity",
+ },
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
- { id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
- { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
+ {
+ id: "android-studio",
+ label: "Android Studio",
+ icon: "android-studio",
+ openWith: "Android Studio",
+ },
+ {
+ id: "sublime-text",
+ label: "Sublime Text",
+ icon: "sublime-text",
+ openWith: "Sublime Text",
+ },
] as const
const WINDOWS_APPS = [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
- { id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
- { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
+ {
+ id: "powershell",
+ label: "PowerShell",
+ icon: "powershell",
+ openWith: "powershell",
+ },
+ {
+ id: "sublime-text",
+ label: "Sublime Text",
+ icon: "sublime-text",
+ openWith: "Sublime Text",
+ },
] as const
const LINUX_APPS = [
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
- { id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
+ {
+ id: "sublime-text",
+ label: "Sublime Text",
+ icon: "sublime-text",
+ openWith: "Sublime Text",
+ },
] as const
type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number]
@@ -213,7 +248,9 @@ export function SessionHeader() {
const view = createMemo(() => layout.view(sessionKey))
const os = createMemo(() => detectOS(platform))
- const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
+ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
+ finder: true,
+ })
const apps = createMemo(() => {
if (os() === "macos") return MAC_APPS
@@ -259,18 +296,34 @@ export function SessionHeader() {
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
const [menu, setMenu] = createStore({ open: false })
+ const [openRequest, setOpenRequest] = createStore({
+ app: undefined as OpenApp | undefined,
+ })
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
const current = createMemo(() => options().find((o) => o.id === prefs.app) ?? options()[0])
+ const opening = createMemo(() => openRequest.app !== undefined)
+
+ createEffect(() => {
+ const value = prefs.app
+ if (options().some((o) => o.id === value)) return
+ setPrefs("app", options()[0]?.id ?? "finder")
+ })
const openDir = (app: OpenApp) => {
+ if (opening() || !canOpen() || !platform.openPath) return
const directory = projectDirectory()
if (!directory) return
- if (!canOpen()) return
const item = options().find((o) => o.id === app)
const openWith = item && "openWith" in item ? item.openWith : undefined
- Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => showRequestError(language, err))
+ setOpenRequest("app", app)
+ platform
+ .openPath(directory, openWith)
+ .catch((err: unknown) => showRequestError(language, err))
+ .finally(() => {
+ setOpenRequest("app", undefined)
+ })
}
const copyPath = () => {
@@ -315,7 +368,9 @@ export function SessionHeader() {
<div class="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
<Icon name="magnifying-glass" size="small" class="icon-base shrink-0 size-4" />
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
- {language.t("session.header.search.placeholder", { project: name() })}
+ {language.t("session.header.search.placeholder", {
+ project: name(),
+ })}
</span>
</div>
@@ -357,12 +412,21 @@ export function SessionHeader() {
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
<Button
variant="ghost"
- class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
+ class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none disabled:!cursor-default"
+ classList={{
+ "bg-surface-raised-base-active": opening(),
+ }}
onClick={() => openDir(current().id)}
+ disabled={opening()}
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
>
<div class="flex size-5 shrink-0 items-center justify-center">
- <AppIcon id={current().icon} class="size-4" />
+ <Show
+ when={opening()}
+ fallback={<AppIcon id={current().icon} class={openIconSize(current().icon)} />}
+ >
+ <Spinner class="size-3.5 text-icon-base" />
+ </Show>
</div>
<span class="text-12-regular text-text-strong">Open</span>
</Button>
@@ -377,7 +441,11 @@ export function SessionHeader() {
as={IconButton}
icon="chevron-down"
variant="ghost"
- class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-hover"
+ disabled={opening()}
+ class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
+ classList={{
+ "bg-surface-raised-base-active": opening(),
+ }}
aria-label={language.t("session.header.open.menu")}
/>
<DropdownMenu.Portal>
@@ -395,6 +463,7 @@ export function SessionHeader() {
{(o) => (
<DropdownMenu.RadioItem
value={o.id}
+ disabled={opening()}
onSelect={() => {
setMenu("open", false)
openDir(o.id)