Compare commits

..

10 Commits

Author SHA1 Message Date
Aaron Iker
24ff7de33d feat: add session menu back 2026-01-08 20:47:10 +01:00
Aaron Iker
f7bf25cea7 feat: provider 2026-01-08 20:46:52 +01:00
Aaron Iker
0732a2b6ce Merge branch 'dev' into new-toolbar-layout 2026-01-08 05:24:30 +01:00
Aaron Iker
74294d7200 fix: web spacing macos 2026-01-08 05:09:14 +01:00
Aaron Iker
f3c62e0723 fix: smaller screens sidebar toggle 2026-01-08 03:39:20 +01:00
Aaron Iker
295d9d16fc fix: toolbar context reactivity 2026-01-08 03:13:06 +01:00
Aaron Iker
2b04837327 feat: traffic lights position mac os 2026-01-08 03:07:46 +01:00
Aaron Iker
f552eea391 feat: loading session header in a portal, show in toolbar 2026-01-08 03:07:32 +01:00
Aaron Iker
f23e3681b7 fix: small style fixes 2026-01-08 03:04:31 +01:00
Aaron Iker
1455976689 fix: avatar not loaded fallback 2026-01-08 03:03:26 +01:00
25 changed files with 551 additions and 500 deletions

View File

@@ -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 }) {
<div class="flex gap-3 items-start">
<div class="relative">
<div
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
class="size-12 rounded-lg 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(),
@@ -108,20 +111,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
onDragLeave={handleDragLeave}
onClick={() => document.getElementById("icon-upload")?.click()}
>
<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>
<ProjectIcon
name={store.name || defaultName()}
projectId={props.project.id}
iconUrl={store.iconUrl}
iconColor={store.color}
class="size-full"
/>
</div>
<Show when={store.iconUrl}>
<button

View File

@@ -0,0 +1,58 @@
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 ProjectIconProps 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 ProjectIcon = (props: ProjectIconProps) => {
const [local, rest] = splitProps(props, [
"name",
"iconUrl",
"iconColor",
"projectId",
"size",
"class",
"classList",
"style",
])
const colors = createMemo(() => getAvatarColors(local.iconColor))
const validSrc = createMemo(() => {
if (local.projectId === OPENCODE_PROJECT_ID) return OPENCODE_FAVICON_URL
return isValidImageUrl(local.iconUrl) ? local.iconUrl : 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}
/>
)
}

View File

@@ -22,7 +22,7 @@ export function SessionLspIndicator() {
return (
<Show when={lspStats().total > 0}>
<Tooltip placement="top" value={tooltipContent()}>
<div class="flex items-center gap-1 px-2 cursor-default select-none">
<div class="pl-2.5 pr-4 flex gap-2 cursor-default select-none items-center justify-center">
<div
classList={{
"size-1.5 rounded-full": true,

View File

@@ -1,35 +1,22 @@
import { createMemo, createResource, Show } from "solid-js"
import { createMemo, Show } from "solid-js"
import { A, useNavigate, 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 { getFilename } from "@opencode-ai/util/path"
import { base64Decode, base64Encode } 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 { Select } from "@opencode-ai/ui/select"
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"
import type { Session } from "@opencode-ai/sdk/v2/client"
import { same } from "@/utils/same"
export function SessionHeader() {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const navigate = useNavigate()
const command = useCommand()
const server = useServer()
const dialog = useDialog()
const sync = useSync()
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
@@ -41,7 +28,6 @@ export function SessionHeader() {
if (!current?.parentID) return undefined
return sync.data.session.find((s) => s.id === current.parentID)
})
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
function navigateToProject(directory: string) {
@@ -64,7 +50,7 @@ export function SessionHeader() {
>
<Icon name="menu" size="small" />
</button>
<div class="px-4 flex items-center justify-between gap-4 w-full">
<div class="px-4 flex items-center justify-between gap-3 w-full">
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
@@ -137,124 +123,10 @@ export function SessionHeader() {
</div>
<Show when={currentSession() && !parentSession()}>
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
<IconButton as={A} href={`/${params.dir}/session`} icon="plus-small" variant="ghost" />
</TooltipKeybind>
</Show>
</div>
<div class="flex items-center gap-3">
<div class="hidden md:flex items-center gap-1">
<Button
size="small"
variant="ghost"
onClick={() => {
dialog.show(() => <DialogSelectServer />)
}}
>
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-success-base": server.healthy() === true,
"bg-icon-critical-base": server.healthy() === false,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<Icon name="server" size="small" class="text-icon-weak" />
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
</Button>
<SessionLspIndicator />
<SessionMcpIndicator />
</div>
<div class="flex items-center gap-1">
<Show when={currentSession()?.summary?.files}>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle review"
keybind={command.keybind("review.toggle")}
>
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.review.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</Show>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle terminal"
keybind={command.keybind("terminal.toggle")}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</div>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{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 (
<Show when={url.latest}>
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
</div>
</header>
)

View File

@@ -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<ComponentProps<"div">> = ({ class: className, ...props }) => {
const command = useCommand()
const platform = usePlatform()
const layout = useLayout()
return (
<div
classList={{
"pl-[80px]": IS_MAC && platform.platform !== "web",
"pl-2": !IS_MAC || platform.platform === "web",
"py-2 min-h-[41px] mx-px bg-background-base border-b border-border-weak-base flex items-center justify-between w-full border-box relative": true,
...(className ? { [className]: true } : {}),
}}
data-tauri-drag-region
{...props}
>
<TooltipKeybind
class="shrink-0 relative z-10 xl:block hidden"
placement="bottom"
title="Toggle sidebar"
keybind={command.keybind("sidebar.toggle")}
>
<Button
variant="ghost"
size="normal"
class="group/sidebar-toggle shrink-0 text-left justify-center align-middle rounded-lg px-1.5"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
size="small"
class="group-hover/sidebar-toggle:hidden"
/>
<Icon
name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
size="small"
class="hidden group-hover/sidebar-toggle:inline-block"
/>
<Icon
name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
size="small"
class="hidden group-active/sidebar-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
<div id={TOOLBAR_PORTAL_ID} class="contents" />
</div>
)
}
export { ToolbarSession } from "./session"

View File

@@ -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<ComponentProps<"div">> = ({ 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 (
<div class={`flex items-center gap-3 absolute right-5 top-1/2 -translate-y-1/2 ${className ?? ""}`} {...props}>
<div class="hidden md:flex items-center gap-1">
<Button
size="small"
variant="ghost"
class="flex gap-2 items-center justify-center"
onClick={() => {
dialog.show(() => <DialogSelectServer />)
}}
>
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-success-base": server.healthy() === true,
"bg-icon-critical-base": server.healthy() === false,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<Icon name="server" size="small" class="text-icon-weak" />
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
</Button>
<SessionLspIndicator />
<SessionMcpIndicator />
</div>
<div class="flex items-center gap-1">
<Show when={currentSession()?.summary?.files}>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle review"
keybind={command.keybind("review.toggle")}
>
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.review.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</Show>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle terminal"
keybind={command.keybind("terminal.toggle")}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon size="small" name="layout-bottom-partial" class="hidden group-hover/terminal-toggle:inline-block" />
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</div>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{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 (
<Show when={url.latest}>
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
)
}

View File

@@ -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 (
<Show when={params.dir} keyed>
<SDKProvider directory={directory()}>
@@ -31,15 +34,33 @@ export default function Layout(props: ParentProps) {
navigate(`/${params.dir}/session/${sessionID}`)
}
const [portalMount, setPortalMount] = createSignal<HTMLElement | null>(null)
onMount(() => {
setPortalMount(document.getElementById(TOOLBAR_PORTAL_ID))
})
const toolbarKey = createMemo(() => params.id ?? "new")
return (
<DataProvider
data={sync.data}
directory={directory()}
onPermissionRespond={respond}
onNavigateToSession={navigateToSession}
>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
<>
<Show when={portalMount()}>
{(mount) => (
<Portal mount={mount()}>
<Show when={toolbarKey()} keyed>
<ToolbarSession />
</Show>
</Portal>
)}
</Show>
<DataProvider
data={sync.data}
directory={directory()}
onPermissionRespond={respond}
onNavigateToSession={navigateToSession}
>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
</>
)
})}
</SyncProvider>

View File

@@ -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 { Mark } from "@opencode-ai/ui/logo"
import { getFilename } from "@opencode-ai/util/path"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
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 { 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 (
<div class="relative size-5 shrink-0 rounded-sm">
<Avatar
fallback={name()}
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
{...getAvatarColors(props.project.icon?.color)}
<ProjectIcon
name={name()}
projectId={props.project.id}
iconUrl={props.project.icon?.url}
iconColor={props.project.icon?.color}
size="small"
class={`size-full ${props.class ?? ""}`}
style={
notifications().length > 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 (
<>
<div
data-session-id={props.session.id}
class="group/session relative w-full rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
>
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
<A
href={`${props.slug}/session/${props.session.id}`}
class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
>
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
<span
classList={{
"text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
"animate-pulse": isWorking(),
}}
>
{props.session.title}
</span>
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
<Switch>
<Match when={isWorking()}>
<Spinner class="size-2.5 mr-0.5" />
</Match>
<Match when={hasPermissions()}>
<div class="size-1.5 mr-1.5 rounded-full bg-surface-warning-strong" />
</Match>
<Match when={hasError()}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
<Match when={notifications().length > 0}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
</Match>
<Match when={true}>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{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")}
</span>
</Match>
</Switch>
</div>
<div
data-session-id={props.session.id}
class="group/session relative w-full rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
>
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
<A
href={`${props.slug}/session/${props.session.id}`}
class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
>
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
<span
classList={{
"text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
"animate-pulse": isWorking(),
}}
>
{props.session.title}
</span>
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
<Switch>
<Match when={isWorking()}>
<Spinner class="size-2.5 mr-0.5" />
</Match>
<Match when={hasPermissions()}>
<div class="size-1.5 mr-1.5 rounded-full bg-surface-warning-strong" />
</Match>
<Match when={hasError()}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
<Match when={notifications().length > 0}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
</Match>
<Match when={true}>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{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")}
</span>
</Match>
</Switch>
</div>
<Show when={props.session.summary?.files}>
<div class="flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${props.session.summary?.files || "No"} file${props.session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div>
</Show>
</A>
</Tooltip>
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
<TooltipKeybind
placement={props.mobile ? "bottom" : "right"}
title="Archive session"
keybind={command.keybind("session.archive")}
>
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
</TooltipKeybind>
</div>
</div>
<Show when={props.session.summary?.files}>
<div class="flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${props.session.summary?.files || "No"} file${props.session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div>
</Show>
</A>
</Tooltip>
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
<TooltipKeybind
placement={props.mobile ? "bottom" : "right"}
title="Archive session"
keybind={command.keybind("session.archive")}
>
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
</TooltipKeybind>
</div>
</>
</div>
)
}
@@ -780,7 +780,7 @@ export default function Layout(props: ParentProps) {
}
}
return (
// @ts-ignore
// @ts-expect-error - SolidJS directive
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Switch>
<Match when={showExpanded()}>
@@ -902,58 +902,7 @@ export default function Layout(props: ParentProps) {
return (
<div class="flex flex-col self-stretch h-full items-center justify-between overflow-hidden min-h-0">
<div class="flex flex-col items-start self-stretch gap-4 min-h-0">
<Show when={!sidebarProps.mobile}>
<div
classList={{
"border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0": true,
"justify-start": expanded(),
}}
>
<A href="/" class="shrink-0 h-8 flex items-center justify-start px-2 w-full" data-tauri-drag-region>
<Mark class="shrink-0" />
</A>
</div>
</Show>
<div class="flex flex-col items-start self-stretch gap-4 px-2 overflow-hidden min-h-0">
<Show when={!sidebarProps.mobile}>
<TooltipKeybind
class="shrink-0"
placement="right"
title="Toggle sidebar"
keybind={command.keybind("sidebar.toggle")}
inactive={expanded()}
>
<Button
variant="ghost"
size="large"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
size="small"
class="group-hover/sidebar-toggle:hidden"
/>
<Icon
name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
size="small"
class="hidden group-hover/sidebar-toggle:inline-block"
/>
<Icon
name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
size="small"
class="hidden group-active/sidebar-toggle:inline-block"
/>
</div>
<Show when={layout.sidebar.opened()}>
<div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
Toggle sidebar
</div>
</Show>
</Button>
</TooltipKeybind>
</Show>
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
@@ -1056,71 +1005,65 @@ export default function Layout(props: ParentProps) {
}
return (
<div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<div class="flex-1 min-h-0 flex">
<div
classList={{
"hidden xl:block": true,
"relative shrink-0": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : "48px" }}
>
<>
<Toolbar />
<div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<div class="flex-1 min-h-0 flex">
<div
classList={{
"@container w-full h-full pb-5 bg-background-base": true,
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base contain-strict": true,
"hidden xl:block": true,
"relative shrink-0": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : "48px" }}
>
<SidebarContent />
</div>
<Show when={layout.sidebar.opened()}>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
min={150}
max={window.innerWidth * 0.3}
collapseThreshold={80}
onResize={layout.sidebar.resize}
onCollapse={layout.sidebar.close}
/>
</Show>
</div>
<div class="xl:hidden">
<div
classList={{
"fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
}}
onClick={(e) => {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<div
classList={{
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pb-5 transition-transform duration-200 ease-out": true,
"translate-x-0": layout.mobileSidebar.opened(),
"-translate-x-full": !layout.mobileSidebar.opened(),
}}
onClick={(e) => e.stopPropagation()}
>
<div class="border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0">
<A
href="/"
class="shrink-0 h-8 flex items-center justify-start px-2 w-full"
onClick={() => layout.mobileSidebar.hide()}
>
<Mark class="shrink-0" />
</A>
<div
classList={{
"@container w-full h-full py-3 bg-background-base": true,
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base contain-strict": true,
}}
>
<SidebarContent />
</div>
<Show when={layout.sidebar.opened()}>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
min={150}
max={window.innerWidth * 0.3}
collapseThreshold={80}
onResize={layout.sidebar.resize}
onCollapse={layout.sidebar.close}
/>
</Show>
</div>
<div class="xl:hidden">
<div
classList={{
"fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
}}
onPointerDown={(e) => {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<div
classList={{
"@container fixed inset-y-0 left-0 top-[41px] z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between py-5 transition-transform duration-200 ease-out": true,
"translate-x-0": layout.mobileSidebar.opened(),
"-translate-x-full": !layout.mobileSidebar.opened(),
}}
onPointerDown={(e) => e.stopPropagation()}
>
<SidebarContent mobile />
</div>
<SidebarContent mobile />
</div>
</div>
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
</div>
<Toast.Region />
</div>
<Toast.Region />
</div>
</>
)
}

View File

@@ -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"
>
<div
@@ -935,7 +935,9 @@ export default function Page() {
{/* Prompt input */}
<div
ref={(el) => (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"
>
<div

View File

@@ -332,7 +332,8 @@ pub fn run() {
{
window_builder = window_builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
.hidden_title(true)
.traffic_light_position(tauri::LogicalPosition::new(16.0, 19.5));
}
let window = window_builder.build().expect("Failed to create window");

View File

@@ -1,24 +1,23 @@
// @refresh reload
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { check, Update } from "@tauri-apps/plugin-updater"
import { AppBaseProviders, AppInterface, Platform, PlatformProvider } from "@opencode-ai/app"
import { Logo } from "@opencode-ai/ui/logo"
import { AsyncStorage } from "@solid-primitives/storage"
import { invoke } from "@tauri-apps/api/core"
import { getCurrentWindow } from "@tauri-apps/api/window"
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
import { relaunch } from "@tauri-apps/plugin-process"
import { AsyncStorage } from "@solid-primitives/storage"
import { open, save } from "@tauri-apps/plugin-dialog"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
import { type as ostype } from "@tauri-apps/plugin-os"
import { relaunch } from "@tauri-apps/plugin-process"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { Store } from "@tauri-apps/plugin-store"
import { Logo } from "@opencode-ai/ui/logo"
import { Suspense, createResource, ParentProps } from "solid-js"
import { check, Update } from "@tauri-apps/plugin-updater"
import { createResource, ParentProps, Show } from "solid-js"
import { render } from "solid-js/web"
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
import pkg from "../package.json"
import { Show } from "solid-js"
import { createMenu } from "./menu"
import { UPDATER_ENABLED } from "./updater"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -269,9 +268,6 @@ root?.addEventListener("mousewheel", (e) => {
render(() => {
return (
<PlatformProvider value={platform}>
{ostype() === "macos" && (
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
)}
<AppBaseProviders>
<ServerGate>
<AppInterface />

View File

@@ -3,8 +3,9 @@ import { useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { pipe, sumBy } from "remeda"
import { useTheme } from "@tui/context/theme"
import { SplitBorder } from "@tui/component/border"
import { SplitBorder, EmptyBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
import { useDirectory } from "../../context/directory"
import { useKeybind } from "../../context/keybind"
const Title = (props: { session: Accessor<Session> }) => {
@@ -32,6 +33,7 @@ export function Header() {
const sync = useSync()
const session = createMemo(() => sync.session.get(route.sessionID)!)
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const cost = createMemo(() => {
const total = pipe(
@@ -97,6 +99,24 @@ export function Header() {
<Title session={session} />
<ContextInfo context={context} cost={cost} />
</box>
<Show when={shareEnabled()}>
<box flexDirection="row" justifyContent="space-between" gap={1}>
<box flexGrow={1} flexShrink={1}>
<Switch>
<Match when={session().share?.url}>
<text fg={theme.textMuted} wrapMode="word">
{session().share!.url}
</text>
</Match>
<Match when={true}>
<text fg={theme.text} wrapMode="word">
/share <span style={{ fg: theme.textMuted }}>copy link</span>
</text>
</Match>
</Switch>
</box>
</box>
</Show>
</Match>
</Switch>
</box>

View File

@@ -1053,24 +1053,7 @@ export function Session() {
<Toast />
</box>
<Show when={sidebarVisible()}>
<Switch>
<Match when={wide()}>
<Sidebar sessionID={route.sessionID} />
</Match>
<Match when={!wide()}>
<box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
alignItems="flex-end"
backgroundColor={RGBA.fromInts(0, 0, 0, 70)}
>
<Sidebar sessionID={route.sessionID} />
</box>
</Match>
</Switch>
<Sidebar sessionID={route.sessionID} />
</Show>
</box>
</context.Provider>
@@ -1535,30 +1518,15 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () =
}
function Bash(props: ToolProps<typeof BashTool>) {
const { theme } = useTheme()
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
const [expanded, setExpanded] = createSignal(false)
const lines = createMemo(() => output().split("\n"))
const overflow = createMemo(() => lines().length > 10)
const limited = createMemo(() => {
if (expanded() || !overflow()) return output()
return [...lines().slice(0, 10), "…"].join("\n")
})
const { theme } = useTheme()
return (
<Switch>
<Match when={props.metadata.output !== undefined}>
<BlockTool
title={"# " + (props.input.description ?? "Shell")}
part={props.part}
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
>
<BlockTool title={"# " + (props.input.description ?? "Shell")} part={props.part}>
<box gap={1}>
<text fg={theme.text}>$ {props.input.command}</text>
<text fg={theme.text}>{limited()}</text>
<Show when={overflow()}>
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
</Show>
<text fg={theme.text}>{output()}</text>
</box>
</BlockTool>
</Match>

View File

@@ -12,7 +12,7 @@ import { useDirectory } from "../../context/directory"
import { useKV } from "../../context/kv"
import { TodoItem } from "../../component/todo-item"
export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
export function Sidebar(props: { sessionID: string }) {
const sync = useSync()
const { theme } = useTheme()
const session = createMemo(() => sync.session.get(props.sessionID)!)
@@ -77,7 +77,6 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
position={props.overlay ? "absolute" : "relative"}
>
<scrollbox flexGrow={1}>
<box flexShrink={0} gap={1} paddingRight={1}>

View File

@@ -13,11 +13,6 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
export const OPENCODE_DISABLE_MODELS_FETCH = truthy("OPENCODE_DISABLE_MODELS_FETCH")
export const OPENCODE_DISABLE_CLAUDE_CODE = truthy("OPENCODE_DISABLE_CLAUDE_CODE")
export const OPENCODE_DISABLE_CLAUDE_CODE_PROMPT =
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT")
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"

View File

@@ -62,10 +62,10 @@ export namespace SystemPrompt {
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
const GLOBAL_RULE_FILES = [path.join(Global.Path.config, "AGENTS.md")]
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
GLOBAL_RULE_FILES.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
}
const GLOBAL_RULE_FILES = [
path.join(Global.Path.config, "AGENTS.md"),
path.join(os.homedir(), ".claude", "CLAUDE.md"),
]
if (Flag.OPENCODE_CONFIG_DIR) {
GLOBAL_RULE_FILES.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))

View File

@@ -7,7 +7,6 @@ import { Log } from "../util/log"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { exists } from "fs/promises"
import { Flag } from "@/flag/flag"
export namespace Skill {
const log = Log.create({ service: "skill" })
@@ -81,24 +80,22 @@ export namespace Skill {
claudeDirs.push(globalClaude)
}
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS) {
for (const dir of claudeDirs) {
const matches = await Array.fromAsync(
CLAUDE_SKILL_GLOB.scan({
cwd: dir,
absolute: true,
onlyFiles: true,
followSymlinks: true,
dot: true,
}),
).catch((error) => {
log.error("failed .claude directory scan for skills", { dir, error })
return []
})
for (const dir of claudeDirs) {
const matches = await Array.fromAsync(
CLAUDE_SKILL_GLOB.scan({
cwd: dir,
absolute: true,
onlyFiles: true,
followSymlinks: true,
dot: true,
}),
).catch((error) => {
log.error("failed .claude directory scan for skills", { dir, error })
return []
})
for (const match of matches) {
await addSkill(match)
}
for (const match of matches) {
await addSkill(match)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -286,18 +286,4 @@ describe("tool.read truncation", () => {
},
})
})
test("large image files are properly attached without error", async () => {
await Instance.provide({
directory: FIXTURES_DIR,
fn: async () => {
const read = await ReadTool.init()
const result = await read.execute({ filePath: path.join(FIXTURES_DIR, "large-image.png") }, ctx)
expect(result.metadata.truncated).toBe(false)
expect(result.attachments).toBeDefined()
expect(result.attachments?.length).toBe(1)
expect(result.attachments?.[0].type).toBe("file")
},
})
})
})

View File

@@ -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;
}

View File

@@ -43,15 +43,14 @@
/* padding: 8px; */
/* padding: 8px 8px 0 8px; */
border: 1px solid
light-dark(
color-mix(in oklch, var(--border-base) 30%, transparent),
color-mix(in oklch, var(--border-base) 50%, transparent)
);
border: 1px solid hsl(from var(--border-base) h s l / 0.2);
border-radius: var(--radius-xl);
background: var(--surface-raised-stronger-non-alpha);
background-clip: padding-box;
box-shadow: var(--shadow-lg);
box-shadow:
0 15px 45px 0 rgba(19, 16, 16, 0.35),
0 3.35px 10.051px 0 rgba(19, 16, 16, 0.25),
0 0.998px 2.993px 0 rgba(19, 16, 16, 0.2);
/* animation: contentHide 300ms ease-in forwards; */
/**/

View File

@@ -3,8 +3,7 @@
min-width: 8rem;
overflow: hidden;
border-radius: var(--radius-md);
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
background-clip: padding-box;
border: 1px solid var(--border-weak-base);
background-color: var(--surface-raised-stronger-non-alpha);
padding: 4px;
box-shadow: var(--shadow-md);

View File

@@ -7,12 +7,9 @@
min-width: 200px;
max-width: 320px;
border-radius: var(--radius-md);
border: 1px solid var(--border-weak-base);
background-color: var(--surface-raised-stronger-non-alpha);
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
background-clip: padding-box;
box-shadow: var(--shadow-md);
transform-origin: var(--kb-popover-content-transform-origin);
&:focus-within {

View File

@@ -47,17 +47,9 @@
--radius-xl: 0.625rem;
--shadow-xs:
0 1px 2px -0.5px light-dark(hsl(0 0% 0% / 0.04), hsl(0 0% 0% / 0.06)),
0 0.5px 1.5px 0 light-dark(hsl(0 0% 0% / 0.025), hsl(0 0% 0% / 0.08)),
0 1px 3px 0 light-dark(hsl(0 0% 0% / 0.05), hsl(0 0% 0% / 0.1));
0 1px 2px -1px rgba(19, 16, 16, 0.04), 0 1px 2px 0 rgba(19, 16, 16, 0.06), 0 1px 3px 0 rgba(19, 16, 16, 0.08);
--shadow-md:
0 6px 12px -2px light-dark(hsl(0 0% 0% / 0.075), hsl(0 0% 0% / 0.1)),
0 4px 8px -2px light-dark(hsl(0 0% 0% / 0.075), hsl(0 0% 0% / 0.15)),
0 1px 2px light-dark(hsl(0 0% 0% / 0.1), hsl(0 0% 0% / 0.15));
--shadow-lg:
0 16px 48px -6px light-dark(hsl(0 0% 0% / 0.05), hsl(0 0% 0% / 0.15)),
0 6px 12px -2px light-dark(hsl(0 0% 0% / 0.025), hsl(0 0% 0% / 0.1)),
0 1px 2.5px light-dark(hsl(0 0% 0% / 0.025), hsl(0 0% 0% / 0.1));
0 6px 8px -4px rgba(19, 16, 16, 0.12), 0 4px 3px -2px rgba(19, 16, 16, 0.12), 0 1px 2px -1px rgba(19, 16, 16, 0.12);
--shadow-xs-border:
0 0 0 1px var(--border-base, rgba(11, 6, 0, 0.2)), 0 1px 2px -1px rgba(19, 16, 16, 0.04),
0 1px 2px 0 rgba(19, 16, 16, 0.06), 0 1px 3px 0 rgba(19, 16, 16, 0.08);

View File

@@ -551,26 +551,23 @@ The opencode CLI takes the following global flags.
OpenCode can be configured using environment variables.
| Variable | Type | Description |
| ------------------------------------- | ------- | ------------------------------------------------ |
| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions |
| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows |
| `OPENCODE_CONFIG` | string | Path to config file |
| `OPENCODE_CONFIG_DIR` | string | Path to config directory |
| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content |
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks |
| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data |
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates |
| `OPENCODE_PERMISSION` | string | Inlined json permissions config |
| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins |
| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads |
| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models |
| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction |
| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Disable reading from `.claude` (prompt + skills) |
| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Disable reading `~/.claude/CLAUDE.md` |
| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disable loading `.claude/skills` |
| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) |
| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools |
| Variable | Type | Description |
| ------------------------------------- | ------- | ---------------------------------------- |
| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions |
| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows |
| `OPENCODE_CONFIG` | string | Path to config file |
| `OPENCODE_CONFIG_DIR` | string | Path to config directory |
| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content |
| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks |
| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data |
| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates |
| `OPENCODE_PERMISSION` | string | Inlined json permissions config |
| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins |
| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads |
| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models |
| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction |
| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) |
| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools |
---