diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 27ce3903cdc..8d389cef7c8 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -3,11 +3,12 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { TextField } from "@opencode-ai/ui/text-field" import { Icon } from "@opencode-ai/ui/icon" +import { Avatar } from "@opencode-ai/ui/avatar" import { createMemo, createSignal, For, Show } from "solid-js" import { createStore } from "solid-js/store" import { useGlobalSDK } from "@/context/global-sdk" import { type LocalProject, getAvatarColors } from "@/context/layout" -import { Avatar } from "@opencode-ai/ui/avatar" +import { ProjectIcon, isValidImageFile } from "@/components/project-icon" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const @@ -33,9 +34,11 @@ export function DialogEditProject(props: { project: LocalProject }) { const [dragOver, setDragOver] = createSignal(false) function handleFileSelect(file: File) { - if (!file.type.startsWith("image/")) return + if (!isValidImageFile(file)) return const reader = new FileReader() - reader.onload = (e) => setStore("iconUrl", e.target?.result as string) + reader.onload = (e) => { + setStore("iconUrl", e.target?.result as string) + } reader.readAsDataURL(file) } @@ -98,7 +101,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
document.getElementById("icon-upload")?.click()} > - - -
- } - > - Project icon - +
-
+
-
- -
- - - - -
- - - - - } - > - {iife(() => { - const [url] = createResource( - () => currentSession(), - async (session) => { - if (!session) return - let shareURL = session.share?.url - if (!shareURL) { - shareURL = await globalSDK.client.session - .share({ sessionID: session.id, directory: projectDirectory() }) - .then((r) => r.data?.share?.url) - .catch((e) => { - console.error("Failed to share session", e) - return undefined - }) - } - return shareURL - }, - { initialValue: "" }, - ) - return ( - - {(shareUrl) => } - - ) - })} - - -
) diff --git a/packages/app/src/components/toolbar/index.tsx b/packages/app/src/components/toolbar/index.tsx new file mode 100644 index 00000000000..b16e8e001b0 --- /dev/null +++ b/packages/app/src/components/toolbar/index.tsx @@ -0,0 +1,66 @@ +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { Button } from "@opencode-ai/ui/button" +import { Icon } from "@opencode-ai/ui/icon" +import type { Component, ComponentProps } from "solid-js" +import { useLayout } from "@/context/layout" +import { useCommand } from "@/context/command" +import { usePlatform } from "@/context/platform" + +const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) + +export const TOOLBAR_PORTAL_ID = "toolbar-content-portal" + +export const Toolbar: Component> = ({ class: className, ...props }) => { + const command = useCommand() + const platform = usePlatform() + const layout = useLayout() + + return ( +
+ + +
+
+ ) +} + +export { ToolbarSession } from "./session" diff --git a/packages/app/src/components/toolbar/session.tsx b/packages/app/src/components/toolbar/session.tsx new file mode 100644 index 00000000000..442e78679a8 --- /dev/null +++ b/packages/app/src/components/toolbar/session.tsx @@ -0,0 +1,147 @@ +import { createMemo, createResource, Show, Component, type ComponentProps } from "solid-js" +import { useParams } from "@solidjs/router" +import { useLayout } from "@/context/layout" +import { useCommand } from "@/context/command" +import { useServer } from "@/context/server" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useSync } from "@/context/sync" +import { useGlobalSDK } from "@/context/global-sdk" +import { base64Decode } from "@opencode-ai/util/encode" +import { iife } from "@opencode-ai/util/iife" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Button } from "@opencode-ai/ui/button" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { Popover } from "@opencode-ai/ui/popover" +import { TextField } from "@opencode-ai/ui/text-field" +import { DialogSelectServer } from "@/components/dialog-select-server" +import { SessionLspIndicator } from "@/components/session-lsp-indicator" +import { SessionMcpIndicator } from "@/components/session-mcp-indicator" + +export const ToolbarSession: Component> = ({ class: className, ...props }) => { + const globalSDK = useGlobalSDK() + const layout = useLayout() + const params = useParams() + const command = useCommand() + const server = useServer() + const dialog = useDialog() + const sync = useSync() + + const projectDirectory = createMemo(() => base64Decode(params.dir ?? "")) + const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id)) + const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") + + return ( +
+ +
+ + + + +
+ + + + + } + > + {iife(() => { + const [url] = createResource( + () => currentSession(), + async (session) => { + if (!session) return + let shareURL = session.share?.url + if (!shareURL) { + shareURL = await globalSDK.client.session + .share({ sessionID: session.id, directory: projectDirectory() }) + .then((r) => r.data?.share?.url) + .catch((e) => { + console.error("Failed to share session", e) + return undefined + }) + } + return shareURL + }, + { initialValue: "" }, + ) + return ( + + {(shareUrl) => } + + ) + })} + + +
+ ) +} diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 39124637c26..87e6cfbc768 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,8 +1,10 @@ -import { createMemo, Show, type ParentProps } from "solid-js" +import { createMemo, createSignal, onMount, Show, type ParentProps } from "solid-js" +import { Portal } from "solid-js/web" import { useNavigate, useParams } from "@solidjs/router" import { SDKProvider, useSDK } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" +import { ToolbarSession, TOOLBAR_PORTAL_ID } from "@/components/toolbar" import { base64Decode } from "@opencode-ai/util/encode" import { DataProvider } from "@opencode-ai/ui/context" @@ -14,6 +16,7 @@ export default function Layout(props: ParentProps) { const directory = createMemo(() => { return base64Decode(params.dir!) }) + return ( @@ -31,15 +34,33 @@ export default function Layout(props: ParentProps) { navigate(`/${params.dir}/session/${sessionID}`) } + const [portalMount, setPortalMount] = createSignal(null) + onMount(() => { + setPortalMount(document.getElementById(TOOLBAR_PORTAL_ID)) + }) + + const toolbarKey = createMemo(() => params.id ?? "new") + return ( - - {props.children} - + <> + + {(mount) => ( + + + + + + )} + + + {props.children} + + ) })} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 85d61d57beb..9b6a3720aa5 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -6,32 +6,31 @@ import { Match, onCleanup, onMount, - ParentProps, Show, Switch, untrack, type JSX, + type ParentProps, } from "solid-js" +import { createStore, produce } from "solid-js/store" import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" -import { useLayout, getAvatarColors, LocalProject } from "@/context/layout" -import { useGlobalSync } from "@/context/global-sync" -import { base64Decode, base64Encode } from "@opencode-ai/util/encode" -import { Avatar } from "@opencode-ai/ui/avatar" -import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import type { Session } from "@opencode-ai/sdk/v2/client" import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" -import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" -import { Spinner } from "@opencode-ai/ui/spinner" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" import { Mark } from "@opencode-ai/ui/logo" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { Spinner } from "@opencode-ai/ui/spinner" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { getFilename } from "@opencode-ai/util/path" -import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Session } from "@opencode-ai/sdk/v2/client" +import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout" +import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" -import { createStore, produce } from "solid-js/store" import { DragDropProvider, DragDropSensors, @@ -57,6 +56,8 @@ import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { useServer } from "@/context/server" +import { Toolbar } from "@/components/toolbar" +import { ProjectIcon } from "@/components/project-icon" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -575,14 +576,15 @@ export default function Layout(props: ParentProps) { const hasError = createMemo(() => notifications().some((n) => n.type === "error")) const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)" - const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" return (
- 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined @@ -669,77 +671,75 @@ export default function Layout(props: ParentProps) { return status?.type === "busy" || status?.type === "retry" }) return ( - <> -
- - -
- - {props.session.title} - -
- - - - - -
- - -
- - 0}> -
- - - - {Math.abs(updated().diffNow().as("seconds")) < 60 - ? "Now" - : updated() - .toRelative({ - style: "short", - unit: ["days", "hours", "minutes"], - }) - ?.replace(" ago", "") - ?.replace(/ days?/, "d") - ?.replace(" min.", "m") - ?.replace(" hr.", "h")} - - - -
+
+ + +
+ + {props.session.title} + +
+ + + + + + ) } @@ -780,7 +780,7 @@ export default function Layout(props: ParentProps) { } } return ( - // @ts-ignore + // @ts-expect-error - SolidJS directive
@@ -902,58 +902,7 @@ export default function Layout(props: ParentProps) { return (
- -
- - - -
-
- - - - - -
-
+ <> + +
+
- +
+ +
+ + +
- - +
{ + if (e.target === e.currentTarget) layout.mobileSidebar.hide() + }} /> - -
-
-
{ - if (e.target === e.currentTarget) layout.mobileSidebar.hide() - }} - /> -
e.stopPropagation()} - > -
- layout.mobileSidebar.hide()} - > - - +
e.stopPropagation()} + > +
-
-
-
{props.children}
+
{props.children}
+
+
- -
+ ) } diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d3d8ef387cb..c2f76d2de78 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -857,7 +857,7 @@ export default function Page() { autoScroll.handleScroll() if (isDesktop()) scheduleScrollSpy(e.currentTarget) }} - onClick={autoScroll.handleInteraction} + onPointerDown={autoScroll.handleInteraction} class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar" >
(promptDock = el)} + ref={(el) => { + promptDock = el + }} class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-8 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none" >
{ render(() => { return ( - {ostype() === "macos" && ( -
- )} diff --git a/packages/ui/src/assets/icons/provider/abacus.svg b/packages/ui/src/assets/icons/provider/abacus.svg new file mode 100644 index 00000000000..121a91d98ef --- /dev/null +++ b/packages/ui/src/assets/icons/provider/abacus.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/icons/provider/friendli.svg b/packages/ui/src/assets/icons/provider/friendli.svg new file mode 100644 index 00000000000..8acb7632df0 --- /dev/null +++ b/packages/ui/src/assets/icons/provider/friendli.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/provider/nano-gpt.svg b/packages/ui/src/assets/icons/provider/nano-gpt.svg new file mode 100644 index 00000000000..f7fbb4cdcde --- /dev/null +++ b/packages/ui/src/assets/icons/provider/nano-gpt.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/components/avatar.css b/packages/ui/src/components/avatar.css index 87be9a50ac6..a685d1dd21b 100644 --- a/packages/ui/src/components/avatar.css +++ b/packages/ui/src/components/avatar.css @@ -22,7 +22,7 @@ [data-component="avatar"][data-size="small"] { width: 1.25rem; height: 1.25rem; - font-size: 0.75rem; + font-size: 0.65rem; line-height: 1; } diff --git a/packages/ui/src/components/provider-icons/sprite.svg b/packages/ui/src/components/provider-icons/sprite.svg index 22e1223a1e2..88406fa8c3c 100644 --- a/packages/ui/src/components/provider-icons/sprite.svg +++ b/packages/ui/src/components/provider-icons/sprite.svg @@ -382,6 +382,12 @@ fill="currentColor" > + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/components/provider-icons/types.ts b/packages/ui/src/components/provider-icons/types.ts index 81fcc3678ac..89fbc0625f5 100644 --- a/packages/ui/src/components/provider-icons/types.ts +++ b/packages/ui/src/components/provider-icons/types.ts @@ -31,6 +31,7 @@ export const iconNames = [ "ollama-cloud", "nvidia", "nebius", + "nano-gpt", "morph", "moonshotai", "moonshotai-cn", @@ -54,6 +55,7 @@ export const iconNames = [ "google-vertex-anthropic", "github-models", "github-copilot", + "friendli", "fireworks-ai", "fastrouter", "deepseek", @@ -73,6 +75,7 @@ export const iconNames = [ "alibaba", "alibaba-cn", "aihubmix", + "abacus", ] as const export type IconName = (typeof iconNames)[number]