|
|
@@ -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)
|