mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-10 02:44:21 +00:00
Compare commits
1 Commits
sqlite2
...
app/open-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2264c93b6b |
@@ -319,7 +319,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
|
||||
onMount(() => {
|
||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
void platform.openLink(store.authorization.url).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -396,7 +396,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
if (store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
void platform.openLink(store.authorization.url).catch(() => undefined)
|
||||
}
|
||||
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
|
||||
@@ -10,7 +10,11 @@ export function Link(props: LinkProps) {
|
||||
const [local, rest] = splitProps(props, ["href", "children"])
|
||||
|
||||
return (
|
||||
<button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}>
|
||||
<button
|
||||
class="text-text-strong underline"
|
||||
onClick={() => void platform.openLink(local.href).catch(() => undefined)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { StatusPopover } from "../status-popover"
|
||||
import { SessionOpenMenu } from "./session-open-menu"
|
||||
|
||||
export function SessionHeader() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
@@ -117,7 +118,7 @@ export function SessionHeader() {
|
||||
function viewShare() {
|
||||
const url = shareUrl()
|
||||
if (!url) return
|
||||
platform.openLink(url)
|
||||
void platform.openLink(url).catch(() => undefined)
|
||||
}
|
||||
|
||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||
@@ -150,6 +151,7 @@ export function SessionHeader() {
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<div class="flex items-center gap-3">
|
||||
<SessionOpenMenu dir={projectDirectory()} />
|
||||
<StatusPopover />
|
||||
<Show when={showShare()}>
|
||||
<div class="flex items-center">
|
||||
|
||||
110
packages/app/src/components/session/session-open-menu.tsx
Normal file
110
packages/app/src/components/session/session-open-menu.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useServer } from "@/context/server"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { FileTypeIcon } from "@opencode-ai/ui/file-type-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
|
||||
export function SessionOpenMenu(props: { dir: string }) {
|
||||
const platform = usePlatform()
|
||||
const server = useServer()
|
||||
const language = useLanguage()
|
||||
|
||||
const enabled = createMemo(
|
||||
() => platform.platform === "desktop" && platform.os === "macos" && server.isLocal() && !!props.dir,
|
||||
)
|
||||
|
||||
const open = (app?: string) => {
|
||||
if (!props.dir) return
|
||||
void platform.openLink(props.dir, app).catch((error) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const copy = () => {
|
||||
if (!props.dir) return
|
||||
navigator.clipboard
|
||||
.writeText(props.dir)
|
||||
.then(() => {
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "check",
|
||||
title: language.t("session.header.copyPath.copied"),
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("session.header.copyPath.copyFailed"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenu.Trigger
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
icon="folder"
|
||||
class="rounded-sm h-[24px] py-1.5 pr-2 pl-2 gap-1.5 border-none shadow-none data-[expanded]:bg-surface-raised-base-active"
|
||||
aria-label={language.t("session.header.open")}
|
||||
>
|
||||
<span class="text-12-regular text-text-strong">{language.t("session.header.open")}</span>
|
||||
<Icon name="chevron-down" size="small" class="icon-base" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1 w-60">
|
||||
<Show when={enabled()}>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||
<DropdownMenu.Item onSelect={() => open("Visual Studio Code")}>
|
||||
<FileTypeIcon id="Vscode" class="size-5" />
|
||||
<DropdownMenu.ItemLabel>VS Code</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => open("Cursor")}>
|
||||
<FileTypeIcon id="Cursor" class="size-5" />
|
||||
<DropdownMenu.ItemLabel>Cursor</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => open("Finder")}>
|
||||
<Icon name="folder" size="small" class="icon-base shrink-0" />
|
||||
<DropdownMenu.ItemLabel>Finder</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => open("Terminal")}>
|
||||
<FileTypeIcon id="Console" class="size-5" />
|
||||
<DropdownMenu.ItemLabel>Terminal</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => open("iTerm")}>
|
||||
<FileTypeIcon id="Console" class="size-5" />
|
||||
<DropdownMenu.ItemLabel>iTerm2</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => open("Ghostty")}>
|
||||
<FileTypeIcon id="Console" class="size-5" />
|
||||
<DropdownMenu.ItemLabel>Ghostty</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => open("Xcode")}>
|
||||
<FileTypeIcon id="Swift" class="size-5" />
|
||||
<DropdownMenu.ItemLabel>Xcode</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => open("Android Studio")}>
|
||||
<FileTypeIcon id="Android" class="size-5" />
|
||||
<DropdownMenu.ItemLabel>Android Studio</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={copy}>
|
||||
<Icon name="copy" size="small" class="icon-base shrink-0" />
|
||||
<DropdownMenu.ItemLabel>{language.t("session.header.copyPath")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -12,8 +12,8 @@ export type Platform = {
|
||||
/** App version */
|
||||
version?: string
|
||||
|
||||
/** Open a URL in the default browser */
|
||||
openLink(url: string): void
|
||||
/** Open a URL/path using the OS (optionally with a specific app) */
|
||||
openLink(url: string, openWith?: string): Promise<void>
|
||||
|
||||
/** Restart the app */
|
||||
restart(): Promise<void>
|
||||
|
||||
@@ -28,7 +28,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
const platform: Platform = {
|
||||
platform: "web",
|
||||
version: pkg.version,
|
||||
openLink(url: string) {
|
||||
async openLink(url: string, _openWith?: string) {
|
||||
window.open(url, "_blank")
|
||||
},
|
||||
back() {
|
||||
|
||||
@@ -469,6 +469,11 @@ export const dict = {
|
||||
|
||||
"session.header.search.placeholder": "Search {{project}}",
|
||||
"session.header.searchFiles": "Search files",
|
||||
"session.header.open": "Open",
|
||||
"session.header.openIn": "Open in",
|
||||
"session.header.copyPath": "Copy Path",
|
||||
"session.header.copyPath.copied": "Copied path",
|
||||
"session.header.copyPath.copyFailed": "Failed to copy path to clipboard",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Server configurations",
|
||||
|
||||
@@ -269,14 +269,14 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
{language.t("error.page.report.prefix")}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center text-text-interactive-base gap-1"
|
||||
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
>
|
||||
<div>{language.t("error.page.report.discord")}</div>
|
||||
<Icon name="discord" class="text-text-interactive-base" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center text-text-interactive-base gap-1"
|
||||
onClick={() => void platform.openLink("https://opencode.ai/desktop-feedback").catch(() => undefined)}
|
||||
>
|
||||
<div>{language.t("error.page.report.discord")}</div>
|
||||
<Icon name="discord" class="text-text-interactive-base" />
|
||||
</button>
|
||||
</div>
|
||||
<Show when={platform.version}>
|
||||
{(version) => (
|
||||
|
||||
@@ -2995,7 +2995,7 @@ export default function Layout(props: ParentProps) {
|
||||
icon="help"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
onClick={() => void platform.openLink("https://opencode.ai/desktop-feedback").catch(() => undefined)}
|
||||
aria-label={language.t("sidebar.help")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [{ "path": "/**", "app": true }]
|
||||
},
|
||||
"deep-link:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-theme",
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
// This file has been generated by Tauri Specta. Do not edit this file manually.
|
||||
|
||||
import { invoke as __TAURI_INVOKE, Channel } from "@tauri-apps/api/core"
|
||||
import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core';
|
||||
|
||||
/** Commands */
|
||||
export const commands = {
|
||||
killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
|
||||
installCli: () => __TAURI_INVOKE<string>("install_cli"),
|
||||
ensureServerReady: () => __TAURI_INVOKE<ServerReadyData>("ensure_server_ready"),
|
||||
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
||||
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||
}
|
||||
killSidecar: () => __TAURI_INVOKE<void>("kill_sidecar"),
|
||||
installCli: () => __TAURI_INVOKE<string>("install_cli"),
|
||||
ensureServerReady: () => __TAURI_INVOKE<ServerReadyData>("ensure_server_ready"),
|
||||
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
||||
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||
};
|
||||
|
||||
/* Types */
|
||||
export type ServerReadyData = {
|
||||
url: string
|
||||
password: string | null
|
||||
}
|
||||
url: string,
|
||||
password: string | null,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { render } from "solid-js/web"
|
||||
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
|
||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
||||
import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
||||
import { openPath, openUrl } from "@tauri-apps/plugin-opener"
|
||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||
import { check, Update } from "@tauri-apps/plugin-updater"
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||
@@ -94,8 +94,10 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||
return result
|
||||
},
|
||||
|
||||
openLink(url: string) {
|
||||
void shellOpen(url).catch(() => undefined)
|
||||
openLink(url: string, openWith?: string) {
|
||||
const isUrl = /^(https?:|mailto:|tel:|opencode:)/.test(url)
|
||||
if (isUrl) return openUrl(url, openWith)
|
||||
return openPath(url, openWith)
|
||||
},
|
||||
|
||||
back() {
|
||||
@@ -359,7 +361,7 @@ render(() => {
|
||||
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
|
||||
if (link?.href) {
|
||||
e.preventDefault()
|
||||
platform.openLink(link.href)
|
||||
void platform.openLink(link.href).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
packages/ui/src/components/file-type-icon.tsx
Normal file
24
packages/ui/src/components/file-type-icon.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Component, JSX } from "solid-js"
|
||||
import { splitProps } from "solid-js"
|
||||
import sprite from "./file-icons/sprite.svg"
|
||||
import type { IconName } from "./file-icons/types"
|
||||
|
||||
export type FileTypeIconProps = JSX.SVGElementTags["svg"] & {
|
||||
id: IconName
|
||||
}
|
||||
|
||||
export const FileTypeIcon: Component<FileTypeIconProps> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["id", "class", "classList"])
|
||||
return (
|
||||
<svg
|
||||
data-component="file-type-icon"
|
||||
{...rest}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
<use href={`${sprite}#${local.id}`} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user