Skip to content
Closed
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
6 changes: 3 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions nix/hashes.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-NczRp8MPppkqP8PQfWMUWJ/Wofvf2YVy5m4i22Pi3jg=",
"aarch64-linux": "sha256-QIxGOu8Fj+sWgc9hKvm1BLiIErxEtd17SPlwZGac9sQ=",
"aarch64-darwin": "sha256-Rb9qbMM+ARn0iBCaZurwcoUBCplbMXEZwrXVKextp3I=",
"x86_64-darwin": "sha256-KVxOKkaVV7W+K4reEk14MTLgmtoqwCYDqDNXNeS6ync="
"x86_64-linux": "sha256-AgHhYsiygxbsBo3JN4HqHXKAwh8n1qeuSCe2qqxlxW4=",
"aarch64-linux": "sha256-h2lpWRQ5EDYnjpqZXtUAp1mxKLQxJ4m8MspgSY8Ev78=",
"aarch64-darwin": "sha256-xnd91+WyeAqn06run2ajsekxJvTMiLsnqNPe/rR8VTM=",
"x86_64-darwin": "sha256-rXpz45IOjGEk73xhP9VY86eOj2CZBg2l1vzwzTIOOOQ="
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.11",
"packageManager": "bun@1.3.13",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop-electron dev",
Expand All @@ -30,7 +30,7 @@
"@effect/opentelemetry": "4.0.0-beta.48",
"@effect/platform-node": "4.0.0-beta.48",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.11",
"@types/bun": "1.3.12",
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
Expand Down
52 changes: 31 additions & 21 deletions packages/app/src/components/dialog-edit-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { type LocalProject, getAvatarColors } from "@/context/layout"
import { getFilename } from "@opencode-ai/shared/util/path"
import { Avatar } from "@opencode-ai/ui/avatar"
import { useLanguage } from "@/context/language"
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"

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

Expand All @@ -26,8 +27,8 @@ export function DialogEditProject(props: { project: LocalProject }) {

const [store, setStore] = createStore({
name: defaultName(),
color: props.project.icon?.color || "pink",
iconUrl: props.project.icon?.override || "",
color: props.project.icon?.color,
iconOverride: props.project.icon?.override,
startup: props.project.commands?.start ?? "",
dragOver: false,
iconHover: false,
Expand All @@ -39,7 +40,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
if (!file.type.startsWith("image/")) return
const reader = new FileReader()
reader.onload = (e) => {
setStore("iconUrl", e.target?.result as string)
setStore("iconOverride", e.target?.result as string)
setStore("iconHover", false)
}
reader.readAsDataURL(file)
Expand Down Expand Up @@ -68,7 +69,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
}

function clearIcon() {
setStore("iconUrl", "")
setStore("iconOverride", "")
}

const saveMutation = useMutation(() => ({
Expand All @@ -81,17 +82,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
projectID: props.project.id,
directory: props.project.worktree,
name,
icon: { color: store.color, override: store.iconUrl },
icon: { color: store.color || "", override: store.iconOverride || "" },
commands: { start },
})
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
dialog.close()
return
}

globalSync.project.meta(props.project.worktree, {
name,
icon: { color: store.color, override: store.iconUrl || undefined },
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
commands: { start: start || undefined },
})
dialog.close()
Expand Down Expand Up @@ -130,21 +131,25 @@ export function DialogEditProject(props: { project: LocalProject }) {
classList={{
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
"border-border-base hover:border-border-strong": !store.dragOver,
"overflow-hidden": !!store.iconUrl,
"overflow-hidden": !!store.iconOverride,
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => {
if (store.iconUrl && store.iconHover) {
if (store.iconOverride && store.iconHover) {
clearIcon()
} else {
iconInput?.click()
}
}}
>
<Show
when={store.iconUrl}
when={getProjectAvatarSource(props.project.id, {
color: store.color,
url: props.project.icon?.url,
override: store.iconOverride,
})}
fallback={
<div class="size-full flex items-center justify-center">
<Avatar
Expand All @@ -155,27 +160,29 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
}
>
<img
src={store.iconUrl}
alt={language.t("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
{(src) => (
<img
src={src()}
alt={language.t("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
)}
</Show>
</div>
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !store.iconUrl,
"opacity-0": !(store.iconHover && !store.iconUrl),
"opacity-100": store.iconHover && !store.iconOverride,
"opacity-0": !(store.iconHover && !store.iconOverride),
}}
>
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
</div>
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !!store.iconUrl,
"opacity-0": !(store.iconHover && !!store.iconUrl),
"opacity-100": store.iconHover && !!store.iconOverride,
"opacity-0": !(store.iconHover && !!store.iconOverride),
}}
>
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
Expand All @@ -198,7 +205,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
</div>

<Show when={!store.iconUrl}>
<Show when={!store.iconOverride}>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
<div class="flex gap-1.5">
Expand All @@ -215,7 +222,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
store.color !== color,
}}
onClick={() => setStore("color", color)}
onClick={() => {
if (store.color === color && !props.project.icon?.url) return
setStore("color", store.color === color ? undefined : color)
}}
>
<Avatar
fallback={store.name || defaultName()}
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/components/prompt-input/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
}

const [head, ...tail] = text.split(" ")
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
const cmd = head?.startsWith("/") || head?.startsWith("$") ? head.slice(1) : undefined
if (cmd && input.sync.data.command.find((item) => item.name === cmd)) {
setBusy()
try {
Expand Down Expand Up @@ -452,7 +452,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
return
}

if (text.startsWith("/")) {
if (text.startsWith("/") || text.startsWith("$")) {
const [cmdName, ...args] = text.split(" ")
const commandName = cmdName.slice(1)
const customCommand = sync.data.command.find((c) => c.name === commandName)
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}

for (const project of projects) {
if (project.icon?.color) continue
if (project.icon?.color || project.icon.url) continue
const worktree = project.worktree
const existing = colors[worktree]
const color = existing ?? pickAvailableColor(used)
Expand Down
14 changes: 9 additions & 5 deletions packages/app/src/pages/layout/sidebar-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import { childSessionOnPath, hasProjectPermissions } from "./helpers"

const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"

export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
return id === OPENCODE_PROJECT_ID
? "https://opencode.ai/favicon.svg"
: icon?.color
? undefined
: icon?.override || icon?.url
}

export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
const globalSync = useGlobalSync()
const notification = useNotification()
Expand All @@ -42,11 +50,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
src={
props.project.id === OPENCODE_PROJECT_ID
? "https://opencode.ai/favicon.svg"
: props.project.icon?.override || props.project.icon?.url
}
src={getProjectAvatarSource(props.project.id, props.project.icon)}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
classList={{ "badge-mask": notify() }}
Expand Down
2 changes: 1 addition & 1 deletion packages/containers/bun-node/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ FROM ${REGISTRY}/build/base:24.04
SHELL ["/bin/bash", "-lc"]

ARG NODE_VERSION=24.4.0
ARG BUN_VERSION=1.3.11
ARG BUN_VERSION=1.3.13

ENV BUN_INSTALL=/opt/bun
ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Expand Down
25 changes: 14 additions & 11 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { Session } from "@tui/routes/session"
import { PromptHistoryProvider } from "./component/prompt/history"
import { FrecencyProvider } from "./component/prompt/frecency"
import { PromptStashProvider } from "./component/prompt/stash"
import { SkillCatalogProvider } from "./context/skills"
import { DialogAlert } from "./ui/dialog-alert"
import { DialogConfirm } from "./ui/dialog-confirm"
import { ToastProvider, useToast } from "./ui/toast"
Expand Down Expand Up @@ -172,17 +173,19 @@ export function tui(input: {
<LocalProvider>
<KeybindProvider>
<PromptStashProvider>
<DialogProvider>
<CommandProvider>
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<App onSnapshot={input.onSnapshot} />
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
</CommandProvider>
</DialogProvider>
<SkillCatalogProvider>
<DialogProvider>
<CommandProvider>
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<App onSnapshot={input.onSnapshot} />
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
</CommandProvider>
</DialogProvider>
</SkillCatalogProvider>
</PromptStashProvider>
</KeybindProvider>
</LocalProvider>
Expand Down
14 changes: 6 additions & 8 deletions packages/opencode/src/cli/cmd/tui/component/dialog-skill.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import { createResource, createMemo } from "solid-js"
import { createMemo, onMount } from "solid-js"
import { useDialog } from "@tui/ui/dialog"
import { useSDK } from "@tui/context/sdk"
import { useSkillCatalog } from "@tui/context/skills"

export type DialogSkillProps = {
onSelect: (skill: string) => void
}

export function DialogSkill(props: DialogSkillProps) {
const dialog = useDialog()
const sdk = useSDK()
const skillCatalog = useSkillCatalog()
dialog.setSize("large")

const [skills] = createResource(async () => {
const result = await sdk.client.app.skills()
return result.data ?? []
onMount(() => {
void skillCatalog.refresh()
})

const options = createMemo<DialogSelectOption<string>[]>(() => {
const list = skills() ?? []
const list = skillCatalog.skills()
const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
return list.map((skill) => ({
title: skill.name.padEnd(maxWidth),
Expand Down
Loading
Loading