Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0ed6011
feat: lint, wording, WIP
aaroniker Jan 13, 2026
ae7e07d
fix: search clear icon contrast
aaroniker Jan 13, 2026
1d8f764
feat: icon button weak color variant
aaroniker Jan 13, 2026
2610fec
fix: workspace icon fix, fallback, clean structure
aaroniker Jan 13, 2026
cffdf33
feat: session scroll snap
aaroniker Jan 13, 2026
9f47fa7
feat: WIP session nav
aaroniker Jan 13, 2026
26bb424
Merge branch 'dev' into desktop-poilsh-styles-ui-ux
aaroniker Jan 15, 2026
5a6c23d
feat: rename project-avatar
aaroniker Jan 15, 2026
6535556
Merge branch 'dev' into desktop-poilsh-styles-ui-ux
aaroniker Jan 16, 2026
0983ee5
Merge branch 'dev' into desktop-poilsh-styles-ui-ux
aaroniker Jan 16, 2026
b6b3867
Update node_modules hash (aarch64-darwin)
actions-user Jan 16, 2026
0296ab2
feat: message nav animation
aaroniker Jan 19, 2026
ecbda74
feat: scroll fade component
aaroniker Jan 19, 2026
97fd10b
feat: scroll fixes, z-index
aaroniker Jan 19, 2026
8abcd13
feat: message nav
aaroniker Jan 19, 2026
e3b78fc
Merge branch 'dev' into desktop-poilsh-styles-ui-ux
aaroniker Jan 19, 2026
ac255c8
feat: transitions, spacing
aaroniker Jan 19, 2026
4658da0
fix: lint
aaroniker Jan 19, 2026
0860300
fix: format
aaroniker Jan 19, 2026
413f808
feat: style improvements
aaroniker Jan 19, 2026
457c246
Merge branch 'dev' into desktop-poilsh-styles-ui-ux
aaroniker Jan 19, 2026
0beb8a0
feat: layout, scroll sidebar
aaroniker Jan 19, 2026
d6dbdb0
feat: small transitions, spacing, polishing
aaroniker Jan 20, 2026
5fdc32c
Merge branch 'dev' into desktop-poilsh-styles-ui-ux
aaroniker Jan 20, 2026
05a0df5
Merge branch 'dev' into desktop-poilsh-styles-ui-ux
aaroniker Jan 20, 2026
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}
/>
)
}
75 changes: 61 additions & 14 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,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 @@ -1255,7 +1255,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 @@ -1273,9 +1273,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 @@ -1291,7 +1291,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 @@ -1567,7 +1567,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Show>
</div>
<div class="relative p-3 flex items-center justify-between">
<div class="flex items-center justify-start gap-0.5">
<div class="flex items-center justify-start gap-1">
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
Expand Down Expand Up @@ -1618,13 +1618,60 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
title="Thinking effort"
keybind={command.keybind("model.variant.cycle")}
>
<Button
variant="ghost"
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
onClick={() => local.model.variant.cycle()}
>
{local.model.variant.current() ?? "Default"}
</Button>
{(() => {
const [text, setText] = createSignal(local.model.variant.current() ?? "Default")
const [animating, setAnimating] = createSignal(false)
let locked = false

const handleClick = async () => {
if (locked) return

local.model.variant.cycle()
const newText = local.model.variant.current() ?? "Default"

if (newText === text()) return

locked = true
setAnimating(true)

// Wait for exit animation
const charCount = text().length
await new Promise((r) => setTimeout(r, charCount * 40 + 400))

// Reset animating before setting new text so @starting-style works
setAnimating(false)
setText(newText)

// Wait for enter animation
const newCharCount = newText.length
await new Promise((r) => setTimeout(r, newCharCount * 40 + 400))

locked = false
}

return (
<Button
variant="ghost"
class="text-text-base _hidden text-12-regular"
onClick={handleClick}
>
<span data-slot="cycle-text" data-animating={animating()}>
<For each={text().split("")}>
{(char, i) =>
char === " " ? (
<span data-slot="space" />
) : (
<span data-slot="char" style={{ "--i": i() }}>
{i() === 0 ? char.toUpperCase() : char}
</span>
)
}
</For>
</span>
<Icon name="chevron-down" size="small" />
</Button>
)
})()}
</TooltipKeybind>
</Show>
<Show when={permission.permissionsEnabled() && params.id}>
Expand Down Expand Up @@ -1700,7 +1747,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
Loading
Loading