Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions packages/app/src/components/dialog-edit-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ 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 { getFilename } from "@opencode-ai/util/path"
import { Avatar } from "@opencode-ai/ui/avatar"
import { ProjectAvatar, isValidImageFile } from "@/components/project-avatar"

const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const

function getFilename(input: string) {
const parts = input.split("/")
return parts[parts.length - 1] || input
}

export function DialogEditProject(props: { project: LocalProject }) {
const dialog = useDialog()
const globalSDK = useGlobalSDK()
Expand All @@ -30,7 +35,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
const [iconHover, setIconHover] = 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)
Expand Down Expand Up @@ -98,7 +103,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
<div class="flex gap-3 items-start">
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
<div
class="relative size-16 rounded-md transition-colors cursor-pointer"
class="size-16 rounded-md overflow-hidden border border-dashed transition-colors cursor-pointer"
classList={{
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
"border-border-base hover:border-border-strong": !dragOver(),
Expand All @@ -115,20 +120,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
}
}}
>
<Show
when={store.iconUrl}
fallback={
<div class="size-full flex items-center justify-center">
<Avatar
fallback={store.name || defaultName()}
{...getAvatarColors(store.color)}
class="size-full"
/>
</div>
}
>
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
</Show>
<ProjectAvatar
name={store.name || defaultName()}
projectId={props.project.id}
iconUrl={store.iconUrl}
iconColor={store.color}
class="size-full"
/>
</div>
<div
style={{
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/dialog-select-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const ModelSelectorPopover: Component<{
const [open, setOpen] = createSignal(false)

return (
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={12}>
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
Expand Down
73 changes: 73 additions & 0 deletions packages/app/src/components/project-avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createMemo, splitProps, type ComponentProps, type JSX } from "solid-js"
import { Avatar } from "@opencode-ai/ui/avatar"
import { getAvatarColors } from "@/context/layout"

const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
const OPENCODE_FAVICON_URL = "https://opencode.ai/favicon.svg"

export interface ProjectAvatarProps extends Omit<ComponentProps<"div">, "children"> {
name: string
iconUrl?: string
iconColor?: string
projectId?: string
size?: "small" | "normal" | "large"
}

export const isValidImageUrl = (url: string | undefined): boolean => {
if (!url) {
return false
}
if (url.startsWith("data:image/x-icon")) {
return false
}
if (url.startsWith("data:image/vnd.microsoft.icon")) {
return false
}
return true
}

export const isValidImageFile = (file: File): boolean => {
if (!file.type.startsWith("image/")) {
return false
}
if (file.type === "image/x-icon" || file.type === "image/vnd.microsoft.icon") {
return false
}
return true
}

export const ProjectAvatar = (props: ProjectAvatarProps) => {
const [local, rest] = splitProps(props, [
"name",
"iconUrl",
"iconColor",
"projectId",
"size",
"class",
"classList",
"style",
])
const colors = createMemo(() => getAvatarColors(local.iconColor))
const validSrc = createMemo(() => {
if (isValidImageUrl(local.iconUrl)) {
return local.iconUrl
}
if (local.projectId === OPENCODE_PROJECT_ID) {
return OPENCODE_FAVICON_URL
}
return undefined
})

return (
<Avatar
fallback={local.name}
src={validSrc()}
size={local.size}
{...colors()}
class={local.class}
classList={local.classList}
style={local.style as JSX.CSSProperties}
{...rest}
/>
)
}
12 changes: 6 additions & 6 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
.abort({
sessionID: params.id!,
})
.catch(() => {})
.catch(() => { })

const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
const text = prompt
Expand Down Expand Up @@ -1245,7 +1245,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {

const optimisticParts = requestParts.map((part) => ({
...part,
sessionID: session.id,
sessionID: session?.id ?? "",
messageID,
})) as unknown as Part[]

Expand All @@ -1263,9 +1263,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const addOptimisticMessage = () => {
setSyncStore(
produce((draft) => {
const messages = draft.message[session.id]
const messages = draft.message[session?.id ?? ""]
if (!messages) {
draft.message[session.id] = [optimisticMessage]
draft.message[session?.id ?? ""] = [optimisticMessage]
} else {
const result = Binary.search(messages, messageID, (m) => m.id)
messages.splice(result.index, 0, optimisticMessage)
Expand All @@ -1281,7 +1281,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const removeOptimisticMessage = () => {
setSyncStore(
produce((draft) => {
const messages = draft.message[session.id]
const messages = draft.message[session?.id ?? ""]
if (messages) {
const result = Binary.search(messages, messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
Expand Down Expand Up @@ -1690,7 +1690,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
disabled={!prompt.dirty() && !working()}
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="h-6 w-4.5"
class="h-6 w-6"
/>
</Tooltip>
</div>
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/components/session/session-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export function SessionHeader() {
<Show when={shareEnabled() && currentSession()}>
<div class="flex items-center">
<Popover
gutter={16}
title="Publish on web"
description={
shareUrl()
Expand Down Expand Up @@ -298,7 +299,7 @@ export function SessionHeader() {
</div>
</Popover>
<Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
<Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top" gutter={8}>
<Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top-end" gutter={12}>
<IconButton
icon={state.copied ? "check" : "copy"}
variant="secondary"
Expand Down
30 changes: 17 additions & 13 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { Persist, persisted } from "@/utils/persist"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
Expand Down Expand Up @@ -60,12 +59,14 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { useCommand, type CommandOption } from "@/context/command"
import { ProjectAvatar } from "@/components/project-avatar"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { navStart } from "@/utils/perf"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogEditProject } from "@/components/dialog-edit-project"
import { Titlebar } from "@/components/titlebar"
import { useServer } from "@/context/server"
import { ScrollReveal } from "@opencode-ai/ui/scroll-reveal"

export default function Layout(props: ParentProps) {
const [store, setStore, , ready] = persisted(
Expand Down Expand Up @@ -187,7 +188,9 @@ export default function Layout(props: ParentProps) {
onClick={stopPropagation}
onTouchStart={stopPropagation}
>
{props.value()}
<ScrollReveal>
{props.value()}
</ScrollReveal>
</span>
}
>
Expand Down Expand Up @@ -1277,15 +1280,16 @@ 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% - 4px) 4px, transparent 5px, black 5.5px)"
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"

return (
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
{...getAvatarColors(props.project.icon?.color)}
<div class={`relative size-8 shrink-0 rounded-sm ${props.class ?? ""}`}>
<div class="size-full rounded-sm overflow-clip">
<ProjectAvatar
name={name()}
projectId={props.project.id}
iconUrl={props.project.icon?.url}
iconColor={props.project.icon?.color}
size="small"
class="size-full rounded"
style={
notifications().length > 0 && props.notify
Expand Down Expand Up @@ -1538,7 +1542,7 @@ export default function Layout(props: ParentProps) {

return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<div use: sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
<div class="px-2 py-1">
<div class="group/workspace relative">
Expand Down Expand Up @@ -1652,7 +1656,7 @@ export default function Layout(props: ParentProps) {
size="large"
onClick={(e: MouseEvent) => {
loadMore()
;(e.currentTarget as HTMLButtonElement).blur()
; (e.currentTarget as HTMLButtonElement).blur()
}}
>
Load more
Expand Down Expand Up @@ -1720,7 +1724,7 @@ export default function Layout(props: ParentProps) {

return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<div use: sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<HoverCard
openDelay={0}
closeDelay={0}
Expand Down Expand Up @@ -1833,7 +1837,7 @@ export default function Layout(props: ParentProps) {
size="large"
onClick={(e: MouseEvent) => {
loadMore()
;(e.currentTarget as HTMLButtonElement).blur()
; (e.currentTarget as HTMLButtonElement).blur()
}}
>
Load more
Expand Down
Loading
Loading