Compare commits

..
Author SHA1 Message Date
opencode e521fee002 release: v1.1.27
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-20 12:13:48 +00:00
Adam 04e60f2b3d fix(app): no flash of home page on start 2026-01-20 06:10:53 -06:00
GitHub Action 23d71e125c ignore: update download stats 2026-01-20 2026-01-20 12:05:55 +00:00
GitHub Action 27406cf8ef chore: generate 2026-01-20 11:40:31 +00:00
Adam 0596b02f19 chore: cleanup 2026-01-20 05:39:54 -06:00
Adam 5145b72c4a chore: cleanup 2026-01-20 05:37:15 -06:00
Adam 347cd8ac63 chore: cleanup 2026-01-20 05:35:24 -06:00
Adam b711ca57f2 fix(app): localStorage quota 2026-01-20 05:21:33 -06:00
Adam 353115a895 fix(app): user message expand on click 2026-01-20 05:21:33 -06:00
Adam 5f0372183a fix(app): persist quota 2026-01-20 05:21:32 -06:00
GitHub Action 616329ae97 chore: generate 2026-01-20 06:33:16 +00:00
Aiden Cline 9706aaf552 rm filetime assertions from patch tool 2026-01-20 00:32:29 -06:00
Brendan Allan 8b379329a6 fix(desktop): completely disable pinch to zoom 2026-01-20 14:07:39 +08:00
Craig JellickandGitHub 68d1755a9e fix: add space toggle hint to tool selection prompt (#9535) 2026-01-19 23:38:26 -06:00
Aiden Cline 419004992d chore: remove duplicate prompt file 2026-01-19 23:22:57 -06:00
Aiden Cline 0d49df46ef fix: ensure truncation handling applies to mcp servers too 2026-01-19 23:19:24 -06:00
James MengandGitHub 36f5ba52e9 fix(batch): update batch tool definition to outline correct value for max tool calls (#9517) 2026-01-19 22:15:02 -06:00
GitHub Action 088b537657 chore: generate 2026-01-20 01:42:14 +00:00
FilipandGitHub 4ddfa86e7f fix(app): message list overflow & scrolling (#9530) 2026-01-19 19:41:42 -06:00
David Hill b91b76e9eb add 8px padding to recent sessions popover 2026-01-20 01:41:36 +00:00
David Hill 6ed656a615 remove top padding from edit project dialog form 2026-01-20 01:36:34 +00:00
David Hill 7b336add88 update session messages popover gutter to 28px 2026-01-20 01:21:36 +00:00
David Hill 7f9ffe57f9 update thinking text styling in desktop app 2026-01-20 00:42:55 +00:00
57 changed files with 605 additions and 1080 deletions
+1
View File
@@ -205,3 +205,4 @@
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
+15 -15
View File
@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -71,7 +71,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -105,7 +105,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -132,7 +132,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -156,7 +156,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -180,7 +180,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -209,7 +209,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -238,7 +238,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -254,7 +254,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.1.26",
"version": "1.1.27",
"bin": {
"opencode": "./bin/opencode",
},
@@ -358,7 +358,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -378,7 +378,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.1.26",
"version": "1.1.27",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.4",
"@tsconfig/node22": "catalog:",
@@ -389,7 +389,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -402,7 +402,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -443,7 +443,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"zod": "catalog:",
},
@@ -454,7 +454,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.1.26",
"version": "1.1.27",
"description": "",
"type": "module",
"exports": {
@@ -3,20 +3,15 @@ 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 { ProjectAvatar, isValidImageFile } from "@/components/project-avatar"
import { getFilename } from "@opencode-ai/util/path"
import { Avatar } from "@opencode-ai/ui/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()
@@ -35,7 +30,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
const [iconHover, setIconHover] = createSignal(false)
function handleFileSelect(file: File) {
if (!isValidImageFile(file)) return
if (!file.type.startsWith("image/")) return
const reader = new FileReader()
reader.onload = (e) => {
setStore("iconUrl", e.target?.result as string)
@@ -87,7 +82,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
return (
<Dialog title="Edit project" class="w-full max-w-[480px] mx-auto">
<form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6">
<form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
<div class="flex flex-col gap-4">
<TextField
autofocus
@@ -103,7 +98,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="size-16 rounded-md overflow-hidden border border-dashed transition-colors cursor-pointer"
class="relative size-16 rounded-md transition-colors cursor-pointer"
classList={{
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
"border-border-base hover:border-border-strong": !dragOver(),
@@ -120,13 +115,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
}
}}
>
<ProjectAvatar
name={store.name || defaultName()}
projectId={props.project.id}
iconUrl={store.iconUrl}
iconColor={store.color}
class="size-full"
/>
<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>
</div>
<div
style={{
@@ -73,7 +73,7 @@ export const ModelSelectorPopover: Component<{
const [open, setOpen] = createSignal(false)
return (
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={12}>
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
<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">
@@ -1,73 +0,0 @@
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}
/>
)
}
+14 -61
View File
@@ -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
@@ -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[]
@@ -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)
@@ -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)
@@ -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-1">
<div class="flex items-center justify-start gap-0.5">
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
@@ -1618,60 +1618,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
title="Thinking effort"
keybind={command.keybind("model.variant.cycle")}
>
{(() => {
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>
)
})()}
<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>
</TooltipKeybind>
</Show>
<Show when={permission.permissionsEnabled() && params.id}>
@@ -1747,7 +1700,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
disabled={!prompt.dirty() && !working()}
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="h-6 w-6"
class="h-6 w-4.5"
/>
</Tooltip>
</div>
@@ -236,7 +236,6 @@ export function SessionHeader() {
<Show when={shareEnabled() && currentSession()}>
<div class="flex items-center">
<Popover
gutter={16}
title="Publish on web"
description={
shareUrl()
@@ -299,7 +298,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-end" gutter={12}>
<Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top" gutter={8}>
<IconButton
icon={state.copied ? "check" : "copy"}
variant="secondary"
+134 -74
View File
@@ -20,6 +20,7 @@ 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"
@@ -34,7 +35,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Dialog } from "@opencode-ai/ui/dialog"
import { getFilename } from "@opencode-ai/util/path"
import { Session, type Message, type TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore, produce, reconcile } from "solid-js/store"
import {
@@ -59,14 +60,12 @@ 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(
@@ -77,6 +76,7 @@ export default function Layout(props: ParentProps) {
activeWorkspace: undefined as string | undefined,
workspaceOrder: {} as Record<string, string[]>,
workspaceName: {} as Record<string, string>,
workspaceBranchName: {} as Record<string, Record<string, string>>,
workspaceExpanded: {} as Record<string, boolean>,
}),
)
@@ -120,6 +120,17 @@ export default function Layout(props: ParentProps) {
})
const editorRef = { current: undefined as HTMLInputElement | undefined }
const autoselecting = createMemo(() => {
if (params.dir) return false
if (initialDir) return false
if (!autoselect()) return false
if (!pageReady()) return true
if (!layoutReady()) return true
const list = layout.projects.list()
if (list.length === 0) return false
return true
})
const editorOpen = (id: string) => editor.active === id
const editorValue = () => editor.value
@@ -188,9 +199,7 @@ export default function Layout(props: ParentProps) {
onClick={stopPropagation}
onTouchStart={stopPropagation}
>
<ScrollReveal>
{props.value()}
</ScrollReveal>
{props.value()}
</span>
}
>
@@ -201,7 +210,10 @@ export default function Layout(props: ParentProps) {
value={editorValue()}
class={props.class}
onInput={(event) => setEditor("value", event.currentTarget.value)}
onKeyDown={(event) => editorKeyDown(event, props.onSave)}
onKeyDown={(event) => {
event.stopPropagation()
editorKeyDown(event, props.onSave)
}}
onBlur={() => closeEditor()}
onPointerDown={stopPropagation}
onClick={stopPropagation}
@@ -461,9 +473,27 @@ export default function Layout(props: ParentProps) {
),
)
const workspaceName = (directory: string) => store.workspaceName[directory]
const workspaceLabel = (directory: string, branch?: string) =>
workspaceName(directory) ?? branch ?? getFilename(directory)
const workspaceKey = (directory: string) => directory.replace(/[\\/]+$/, "")
const workspaceName = (directory: string, projectId?: string, branch?: string) => {
const key = workspaceKey(directory)
const direct = store.workspaceName[key] ?? store.workspaceName[directory]
if (direct) return direct
if (!projectId) return
if (!branch) return
return store.workspaceBranchName[projectId]?.[branch]
}
const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => {
const key = workspaceKey(directory)
setStore("workspaceName", (prev) => ({ ...(prev ?? {}), [key]: next }))
if (!projectId) return
if (!branch) return
setStore("workspaceBranchName", projectId, (prev) => ({ ...(prev ?? {}), [branch]: next }))
}
const workspaceLabel = (directory: string, branch?: string, projectId?: string) =>
workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
const isWorkspaceEditing = () => editor.active.startsWith("workspace:")
@@ -888,10 +918,10 @@ export default function Layout(props: ParentProps) {
})
}
const renameWorkspace = (directory: string, next: string) => {
const current = workspaceName(directory) ?? getFilename(directory)
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory)
if (current === next) return
setStore("workspaceName", directory, next)
setWorkspaceName(directory, next, projectId, branch)
}
function closeProject(directory: string) {
@@ -1280,16 +1310,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% - 4px) 4px, transparent 5px, black 5.5px)"
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
return (
<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"
<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?.override}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
style={
notifications().length > 0 && props.notify
@@ -1352,7 +1381,7 @@ export default function Layout(props: ParentProps) {
})
const hoverMessages = createMemo(() =>
sessionStore.message[props.session.id]?.filter((message) => message.role === "user") as UserMessage[],
sessionStore.message[props.session.id]?.filter((message) => message.role === "user"),
)
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened())
@@ -1425,7 +1454,7 @@ export default function Layout(props: ParentProps) {
</Tooltip>
}
>
<HoverCard openDelay={150} closeDelay={100} placement="right" gutter={28} trigger={item}>
<HoverCard openDelay={150} closeDelay={100} placement="right-start" gutter={28} trigger={item}>
<Show when={hoverReady()} fallback={<div class="text-12-regular text-text-weak">Loading messages</div>}>
<MessageNav
messages={hoverMessages() ?? []}
@@ -1495,7 +1524,7 @@ export default function Layout(props: ParentProps) {
const [workspaceStore] = globalSync.child(directory)
const kind = directory === project.worktree ? "local" : "sandbox"
const name = workspaceLabel(directory, workspaceStore.vcs?.branch)
const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
return `${kind} : ${name}`
})
@@ -1512,6 +1541,7 @@ export default function Layout(props: ParentProps) {
const sortable = createSortable(props.directory)
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory)
const [menuOpen, setMenuOpen] = createSignal(false)
const [pendingRename, setPendingRename] = createSignal(false)
const slug = createMemo(() => base64Encode(props.directory))
const sessions = createMemo(() =>
workspaceStore.session
@@ -1521,8 +1551,9 @@ export default function Layout(props: ParentProps) {
)
const local = createMemo(() => props.directory === props.project.worktree)
const workspaceValue = createMemo(() => {
const name = workspaceStore.vcs?.branch ?? getFilename(props.directory)
return workspaceName(props.directory) ?? name
const branch = workspaceStore.vcs?.branch
const name = branch ?? getFilename(props.directory)
return workspaceName(props.directory, props.project.id, branch) ?? name
})
const open = createMemo(() => store.workspaceExpanded[props.directory] ?? true)
const loading = createMemo(() => open() && workspaceStore.status !== "complete" && sessions().length === 0)
@@ -1541,50 +1572,61 @@ export default function Layout(props: ParentProps) {
if (editorOpen(`workspace:${props.directory}`)) closeEditor()
}
const header = () => (
<div class="flex items-center gap-1 min-w-0 flex-1">
<div class="flex items-center justify-center shrink-0 size-6">
<Icon name="branch" size="small" />
</div>
<span class="text-14-medium text-text-base shrink-0">{local() ? "local" : "sandbox"} :</span>
<Show
when={!local()}
fallback={
<span class="text-14-medium text-text-base min-w-0 truncate">
{workspaceStore.vcs?.branch ?? getFilename(props.directory)}
</span>
}
>
<InlineEditor
id={`workspace:${props.directory}`}
value={workspaceValue}
onSave={(next) => {
const trimmed = next.trim()
if (!trimmed) return
renameWorkspace(props.directory, trimmed, props.project.id, workspaceStore.vcs?.branch)
setEditor("value", workspaceValue())
}}
class="text-14-medium text-text-base min-w-0 truncate"
displayClass="text-14-medium text-text-base min-w-0 truncate"
editing={workspaceEditActive()}
stopPropagation={false}
openOnDblClick={false}
/>
</Show>
<Icon
name={open() ? "chevron-down" : "chevron-right"}
size="small"
class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/workspace:opacity-100 group-focus-within/workspace:opacity-100"
/>
</div>
)
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">
<div class="flex items-center gap-1">
<Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
<div class="flex items-center gap-1 min-w-0 flex-1">
<div class="flex items-center justify-center shrink-0 size-6">
<Icon name="branch" size="small" />
</div>
<span class="text-14-medium text-text-base shrink-0">{local() ? "local" : "sandbox"} :</span>
<Show
when={!local()}
fallback={
<span class="text-14-medium text-text-base min-w-0 truncate">
{workspaceStore.vcs?.branch ?? getFilename(props.directory)}
</span>
}
>
<InlineEditor
id={`workspace:${props.directory}`}
value={workspaceValue}
onSave={(next) => {
const trimmed = next.trim()
if (!trimmed) return
renameWorkspace(props.directory, trimmed)
setEditor("value", workspaceValue())
}}
class="text-14-medium text-text-base min-w-0 truncate"
displayClass="text-14-medium text-text-base min-w-0 truncate"
editing={workspaceEditActive()}
stopPropagation={false}
openOnDblClick={false}
/>
</Show>
<Icon
name={open() ? "chevron-down" : "chevron-right"}
size="small"
class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/workspace:opacity-100 group-focus-within/workspace:opacity-100"
/>
</div>
</Collapsible.Trigger>
<Show
when={workspaceEditActive()}
fallback={
<Collapsible.Trigger class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover">
{header()}
</Collapsible.Trigger>
}
>
<div class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md">{header()}</div>
</Show>
<div
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
classList={{
@@ -1599,21 +1641,37 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" class="size-6 rounded-md" />
</Tooltip>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Content
onCloseAutoFocus={(event) => {
if (!pendingRename()) return
event.preventDefault()
setPendingRename(false)
openEditor(`workspace:${props.directory}`, workspaceValue())
}}
>
<DropdownMenu.Item onSelect={() => navigate(`/${slug()}/session`)}>
<DropdownMenu.ItemLabel>New session</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={local()}
onSelect={() => {
setPendingRename(true)
setMenuOpen(false)
}}
>
<DropdownMenu.ItemLabel>Rename</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={local()}
onSelect={() => dialog.show(() => <DialogResetWorkspace directory={props.directory} />)}
>
<DropdownMenu.ItemLabel>Reset workspace</DropdownMenu.ItemLabel>
<DropdownMenu.ItemLabel>Reset</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={local()}
onSelect={() => dialog.show(() => <DialogDeleteWorkspace directory={props.directory} />)}
>
<DropdownMenu.ItemLabel>Delete workspace</DropdownMenu.ItemLabel>
<DropdownMenu.ItemLabel>Delete</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
@@ -1657,7 +1715,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
@@ -1685,7 +1743,7 @@ export default function Layout(props: ParentProps) {
const label = (directory: string) => {
const [data] = globalSync.child(directory)
const kind = directory === props.project.worktree ? "local" : "sandbox"
const name = workspaceLabel(directory, data.vcs?.branch)
const name = workspaceLabel(directory, data.vcs?.branch, props.project.id)
return `${kind} : ${name}`
}
@@ -1725,7 +1783,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}
@@ -1734,7 +1792,7 @@ export default function Layout(props: ParentProps) {
trigger={trigger}
onOpenChange={setOpen}
>
<div class="-m-3 flex flex-col w-72">
<div class="-m-3 p-2 flex flex-col w-72">
<div class="px-4 pt-2 pb-1 text-14-medium text-text-strong truncate">{displayName(props.project)}</div>
<div class="px-4 pb-2 text-12-medium text-text-weak">Recent sessions</div>
<div class="px-2 pb-2 flex flex-col gap-2">
@@ -1823,7 +1881,7 @@ export default function Layout(props: ParentProps) {
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar"
style={{ "overflow-anchor": "none" }}
>
<nav class="flex flex-col gap-2 px-2">
<nav class="flex flex-col gap-1 px-2">
<Show when={loading()}>
<SessionSkeleton />
</Show>
@@ -1838,7 +1896,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
@@ -2147,7 +2205,9 @@ export default function Layout(props: ParentProps) {
"xl:border-l xl:rounded-tl-sm": !layout.sidebar.opened(),
}}
>
{props.children}
<Show when={!autoselecting()} fallback={<div class="size-full" />}>
{props.children}
</Show>
</main>
</div>
<Toast.Region />
+74 -65
View File
@@ -570,7 +570,7 @@ export default function Page() {
const sessionID = params.id
if (!sessionID) return
if (status()?.type !== "idle") {
await sdk.client.session.abort({ sessionID }).catch(() => { })
await sdk.client.session.abort({ sessionID }).catch(() => {})
}
const revert = info()?.revert?.messageID
// Find the last user message that's not already reverted
@@ -653,69 +653,69 @@ export default function Page() {
},
...(sync.data.config.share !== "disabled"
? [
{
id: "session.share",
title: "Share session",
description: "Share this session and copy the URL to clipboard",
category: "Session",
slash: "share",
disabled: !params.id || !!info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.share({ sessionID: params.id })
.then((res) => {
navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
{
id: "session.share",
title: "Share session",
description: "Share this session and copy the URL to clipboard",
category: "Session",
slash: "share",
disabled: !params.id || !!info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.share({ sessionID: params.id })
.then((res) => {
navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
showToast({
title: "Failed to copy URL to clipboard",
variant: "error",
}),
)
})
.then(() =>
showToast({
title: "Failed to copy URL to clipboard",
title: "Session shared",
description: "Share URL copied to clipboard!",
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to share session",
description: "An error occurred while sharing the session",
variant: "error",
}),
)
})
.then(() =>
showToast({
title: "Session shared",
description: "Share URL copied to clipboard!",
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to share session",
description: "An error occurred while sharing the session",
variant: "error",
}),
)
},
},
},
{
id: "session.unshare",
title: "Unshare session",
description: "Stop sharing this session",
category: "Session",
slash: "unshare",
disabled: !params.id || !info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.unshare({ sessionID: params.id })
.then(() =>
showToast({
title: "Session unshared",
description: "Session unshared successfully!",
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to unshare session",
description: "An error occurred while unsharing the session",
variant: "error",
}),
)
{
id: "session.unshare",
title: "Unshare session",
description: "Stop sharing this session",
category: "Session",
slash: "unshare",
disabled: !params.id || !info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.unshare({ sessionID: params.id })
.then(() =>
showToast({
title: "Session unshared",
description: "Session unshared successfully!",
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to unshare session",
description: "An error occurred while unsharing the session",
variant: "error",
}),
)
},
},
},
]
]
: []),
])
@@ -824,10 +824,22 @@ export default function Page() {
})
const isWorking = createMemo(() => status().type !== "idle")
const autoScroll = createAutoScroll({
working: isWorking,
working: () => true,
})
createEffect(
on(
isWorking,
(working, prev) => {
if (!working || prev) return
autoScroll.forceScrollToBottom()
},
{ defer: true },
),
)
let scrollSpyFrame: number | undefined
let scrollSpyTarget: HTMLDivElement | undefined
@@ -1265,7 +1277,7 @@ export default function Page() {
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
}}
onClick={autoScroll.handleInteraction}
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar snap-both snap-mandatory"
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
style={{ "--session-title-height": info()?.title ? "40px" : "0px" }}
>
<Show when={info()?.title}>
@@ -1332,17 +1344,14 @@ export default function Page() {
navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" })
})
}
return (
<div
id={anchor(message.id)}
data-message-id={message.id}
classList={{
"min-w-0 w-full max-w-full snap-both": true,
"min-w-0 w-full max-w-full": true,
"md:max-w-200": !showTabs(),
"last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
platform.platform !== "desktop",
"last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
platform.platform === "desktop",
}}
>
<SessionTurn
+139 -6
View File
@@ -16,6 +16,83 @@ type PersistTarget = {
const LEGACY_STORAGE = "default.dat"
const GLOBAL_STORAGE = "opencode.global.dat"
const LOCAL_PREFIX = "opencode."
const fallback = { disabled: false }
const cache = new Map<string, string>()
function quota(error: unknown) {
if (error instanceof DOMException) {
if (error.name === "QuotaExceededError") return true
if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true
if (error.name === "QUOTA_EXCEEDED_ERR") return true
if (error.code === 22 || error.code === 1014) return true
return false
}
if (!error || typeof error !== "object") return false
const name = (error as { name?: string }).name
if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return true
if (name && /quota/i.test(name)) return true
const code = (error as { code?: number }).code
if (code === 22 || code === 1014) return true
const message = (error as { message?: string }).message
if (typeof message !== "string") return false
if (/quota/i.test(message)) return true
return false
}
type Evict = { key: string; size: number }
function evict(storage: Storage, keep: string, value: string) {
const total = storage.length
const indexes = Array.from({ length: total }, (_, index) => index)
const items: Evict[] = []
for (const index of indexes) {
const name = storage.key(index)
if (!name) continue
if (!name.startsWith(LOCAL_PREFIX)) continue
if (name === keep) continue
const stored = storage.getItem(name)
items.push({ key: name, size: stored?.length ?? 0 })
}
items.sort((a, b) => b.size - a.size)
for (const item of items) {
storage.removeItem(item.key)
try {
storage.setItem(keep, value)
return true
} catch (error) {
if (!quota(error)) throw error
}
}
return false
}
function write(storage: Storage, key: string, value: string) {
try {
storage.setItem(key, value)
return true
} catch (error) {
if (!quota(error)) throw error
}
try {
storage.removeItem(key)
storage.setItem(key, value)
return true
} catch (error) {
if (!quota(error)) throw error
}
return evict(storage, key, value)
}
function snapshot(value: unknown) {
return JSON.parse(JSON.stringify(value)) as unknown
@@ -67,10 +144,66 @@ function workspaceStorage(dir: string) {
function localStorageWithPrefix(prefix: string): SyncStorage {
const base = `${prefix}:`
const item = (key: string) => base + key
return {
getItem: (key) => localStorage.getItem(base + key),
setItem: (key, value) => localStorage.setItem(base + key, value),
removeItem: (key) => localStorage.removeItem(base + key),
getItem: (key) => {
const name = item(key)
const cached = cache.get(name)
if (fallback.disabled && cached !== undefined) return cached
const stored = localStorage.getItem(name)
if (stored === null) return cached ?? null
cache.set(name, stored)
return stored
},
setItem: (key, value) => {
const name = item(key)
cache.set(name, value)
if (fallback.disabled) return
try {
if (write(localStorage, name, value)) return
} catch {
fallback.disabled = true
return
}
fallback.disabled = true
},
removeItem: (key) => {
const name = item(key)
cache.delete(name)
if (fallback.disabled) return
localStorage.removeItem(name)
},
}
}
function localStorageDirect(): SyncStorage {
return {
getItem: (key) => {
const cached = cache.get(key)
if (fallback.disabled && cached !== undefined) return cached
const stored = localStorage.getItem(key)
if (stored === null) return cached ?? null
cache.set(key, stored)
return stored
},
setItem: (key, value) => {
cache.set(key, value)
if (fallback.disabled) return
try {
if (write(localStorage, key, value)) return
} catch {
fallback.disabled = true
return
}
fallback.disabled = true
},
removeItem: (key) => {
cache.delete(key)
if (fallback.disabled) return
localStorage.removeItem(key)
},
}
}
@@ -99,7 +232,7 @@ export function removePersisted(target: { storage?: string; key: string }) {
}
if (!target.storage) {
localStorage.removeItem(target.key)
localStorageDirect().removeItem(target.key)
return
}
@@ -120,12 +253,12 @@ export function persisted<T>(
const currentStorage = (() => {
if (isDesktop) return platform.storage?.(config.storage)
if (!config.storage) return localStorage
if (!config.storage) return localStorageDirect()
return localStorageWithPrefix(config.storage)
})()
const legacyStorage = (() => {
if (!isDesktop) return localStorage
if (!isDesktop) return localStorageDirect()
if (!config.storage) return platform.storage?.()
return platform.storage?.(LEGACY_STORAGE)
})()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.1.26",
"version": "1.1.27",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.1.26",
"version": "1.1.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.1.26",
"version": "1.1.27",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.1.26",
"version": "1.1.27",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.1.26",
"version": "1.1.27",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -19,7 +19,7 @@
"url": "/",
"decorations": true,
"dragDropEnabled": false,
"zoomHotkeysEnabled": true,
"zoomHotkeysEnabled": false,
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"trafficLightPosition": { "x": 12.0, "y": 18.0 }
+16 -5
View File
@@ -1,4 +1,5 @@
// @refresh reload
import "./webview-zoom"
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
@@ -94,6 +95,21 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
const apiCache = new Map<string, AsyncStorage & { flush: () => Promise<void> }>()
const memoryCache = new Map<string, StoreLike>()
const flushAll = async () => {
const apis = Array.from(apiCache.values())
await Promise.all(apis.map((api) => api.flush().catch(() => undefined)))
}
if ("addEventListener" in globalThis) {
const handleVisibility = () => {
if (document.visibilityState !== "hidden") return
void flushAll()
}
window.addEventListener("pagehide", () => void flushAll())
document.addEventListener("visibilitychange", handleVisibility)
}
const createMemoryStore = () => {
const data = new Map<string, string>()
const store: StoreLike = {
@@ -303,11 +319,6 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
createMenu()
// Stops mousewheel events from reaching Tauri's pinch-to-zoom handler
root?.addEventListener("mousewheel", (e) => {
e.stopPropagation()
})
render(() => {
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
const platform = createPlatform(() => serverPassword())
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { invoke } from "@tauri-apps/api/core"
import { type as ostype } from "@tauri-apps/plugin-os"
const OS_NAME = ostype()
let zoomLevel = 1
const MAX_ZOOM_LEVEL = 10
const MIN_ZOOM_LEVEL = 0.2
window.addEventListener("keydown", (event) => {
if (OS_NAME === "macos" ? event.metaKey : event.ctrlKey) {
if (event.key === "-") {
zoomLevel -= 0.2
} else if (event.key === "=" || event.key === "+") {
zoomLevel += 0.2
} else if (event.key === "0") {
zoomLevel = 1
} else {
return
}
zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
invoke("plugin:webview|set_webview_zoom", {
value: zoomLevel,
})
}
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.1.26",
"version": "1.1.27",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.1.26"
version = "1.1.27"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.1.26",
"version": "1.1.27",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.26",
"version": "1.1.27",
"name": "opencode",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -134,7 +134,7 @@ const AgentCreateCommand = cmd({
selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS
} else {
const result = await prompts.multiselect({
message: "Select tools to enable",
message: "Select tools to enable (Space to toggle)",
options: AVAILABLE_TOOLS.map((tool) => ({
label: tool,
value: tool,
+10 -2
View File
@@ -44,6 +44,7 @@ import { SessionStatus } from "./status"
import { LLM } from "./llm"
import { iife } from "@/util/iife"
import { Shell } from "@/shell/shell"
import { Truncate } from "@/tool/truncation"
// @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -801,10 +802,17 @@ export namespace SessionPrompt {
}
}
const truncated = await Truncate.output(textParts.join("\n\n"), {}, input.agent)
const metadata = {
...(result.metadata ?? {}),
truncated: truncated.truncated,
...(truncated.truncated && { outputPath: truncated.outputPath }),
}
return {
title: "",
metadata: result.metadata ?? {},
output: textParts.join("\n\n"),
metadata,
output: truncated.content,
attachments,
content: result.content, // directly return content to preserve ordering when outputting to model
}
@@ -1,73 +0,0 @@
You are OpenCode, the best coding agent on the planet.
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
## Editing constraints
- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
- Only add comments if they are necessary to make a non-obvious block easier to understand.
- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase).
## Tool usage
- Prefer specialized tools over shell for file operations:
- Use Read to view files, Edit to modify files, and Write only when needed.
- Use Glob to find files by name and Grep to search file contents.
- Use Bash for terminal operations (git, bun, builds, tests, running scripts).
- Run tool calls in parallel when neither call needs the others output; otherwise run sequentially.
## Git and workspace hygiene
- You may be in a dirty git worktree.
* NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.
* If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.
* If the changes are in files you've touched recently, you should read carefully and understand how you can work with the changes rather than reverting them.
* If the changes are in unrelated files, just ignore them and don't revert them.
- Do not amend commits unless explicitly requested.
- **NEVER** use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested or approved by the user.
## Frontend tasks
When doing frontend design tasks, avoid collapsing into bland, generic layouts.
Aim for interfaces that feel intentional and deliberate.
- Typography: Use expressive, purposeful fonts and avoid default stacks (Inter, Roboto, Arial, system).
- Color & Look: Choose a clear visual direction; define CSS variables; avoid purple-on-white defaults. No purple bias or dark mode bias.
- Motion: Use a few meaningful animations (page-load, staggered reveals) instead of generic micro-motions.
- Background: Don't rely on flat, single-color backgrounds; use gradients, shapes, or subtle patterns to build atmosphere.
- Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs.
- Ensure the page loads properly on both desktop and mobile.
Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language.
## Presenting your work and final message
You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value.
- Default: be very concise; friendly coding teammate tone.
- Ask only when needed; suggest ideas; mirror the user's style.
- For substantial work, summarize clearly; follow finalanswer formatting.
- Skip heavy formatting for simple confirmations.
- Don't dump large files you've written; reference paths only.
- No "save/copy this file" - User is on the same machine.
- Offer logical next steps (tests, commits, build) briefly; add verify steps if you couldn't do something.
- For code changes:
* Lead with a quick explanation of the change, and then give more details on the context covering where and why a change was made. Do not start this explanation with "summary", just jump right in.
* If there are natural next steps the user may want to take, suggest them at the end of your response. Do not make suggestions if there are no natural next steps.
* When suggesting multiple options, use numeric lists for the suggestions so the user can quickly respond with a single number.
- The user does not command execution outputs. When asked to show the output of a command (e.g. `git show`), relay the important details in your answer or summarize the key lines so the user understands the result.
## Final answer structure and style guidelines
- Plain text; CLI handles styling. Use structure only when it helps scanability.
- Headers: optional; short Title Case (1-3 words) wrapped in **…**; no blank line before the first bullet; add only if they truly help.
- Bullets: use - ; merge related points; keep to one line when possible; 46 per list ordered by importance; keep phrasing consistent.
- Monospace: backticks for commands/paths/env vars/code ids and inline examples; use for literal keyword bullets; never combine with **.
- Code samples or multi-line snippets should be wrapped in fenced code blocks; include an info string as often as possible.
- Structure: group related bullets; order sections general → specific → supporting; for subsections, start with a bolded keyword bullet, then items; match complexity to the task.
- Tone: collaborative, concise, factual; present tense, active voice; selfcontained; no "above/below"; parallel wording.
- Don'ts: no nested bullets/hierarchies; no ANSI codes; don't cram unrelated keywords; keep keyword lists short—wrap/reformat if long; avoid naming formatting styles in answers.
- Adaptation: code explanations → precise, structured with code refs; simple tasks → lead with outcome; big changes → logical walkthrough + rationale + next actions; casual one-offs → plain sentences, no headers/bullets.
- File References: When referencing files in your response follow the below rules:
* Use inline code to make file paths clickable.
* Each reference should have a stand alone path. Even if it's the same file.
* Accepted: absolute, workspacerelative, a/ or b/ diff prefixes, or bare filename/suffix.
* Optionally include line/column (1based): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
* Do not use URIs like file://, vscode://, or https://.
* Do not provide range of lines
* Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5
@@ -5,6 +5,7 @@ You are an interactive CLI tool that helps users with software engineering tasks
## Editing constraints
- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
- Only add comments if they are necessary to make a non-obvious block easier to understand.
- Try to use apply_patch for single file edits, but it is fine to explore other options to make the edit if it does not work well. Do not use apply_patch for changes that are auto-generated (i.e. generating package.json or running a lint or format command like gofmt) or when scripting is more efficient (such as search and replacing a string across a codebase).
## Tool usage
- Prefer specialized tools over shell for file operations:
+2 -3
View File
@@ -13,8 +13,7 @@ import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_CODEX from "./prompt/codex.txt"
import PROMPT_CODEX_INSTRUCTIONS from "./prompt/codex_header.txt"
import PROMPT_CODEX from "./prompt/codex_header.txt"
import type { Provider } from "@/provider/provider"
import { Flag } from "@/flag/flag"
@@ -25,7 +24,7 @@ export namespace SystemPrompt {
}
export function instructions() {
return PROMPT_CODEX_INSTRUCTIONS.trim()
return PROMPT_CODEX.trim()
}
export function provider(model: Provider.Model) {
@@ -2,7 +2,6 @@ import z from "zod"
import * as path from "path"
import * as fs from "fs/promises"
import { Tool } from "./tool"
import { FileTime } from "../file/time"
import { Bus } from "../bus"
import { FileWatcher } from "../file/watcher"
import { Instance } from "../project/instance"
@@ -96,8 +95,6 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
throw new Error(`apply_patch verification failed: Failed to read file to update: ${filePath}`)
}
// Read file and update time tracking (like edit tool does)
await FileTime.assert(ctx.sessionID, filePath)
const oldContent = await fs.readFile(filePath, "utf-8")
let newContent = oldContent
@@ -202,12 +199,6 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
changedFiles.push(change.filePath)
break
}
// Update file time tracking
FileTime.read(ctx.sessionID, change.filePath)
if (change.movePath) {
FileTime.read(ctx.sessionID, change.movePath)
}
}
// Publish file change events
+1 -1
View File
@@ -6,7 +6,7 @@ Payload Format (JSON array):
[{"tool": "read", "parameters": {"filePath": "src/index.ts", "limit": 350}},{"tool": "grep", "parameters": {"pattern": "Session\\.updatePart", "include": "src/**/*.ts"}},{"tool": "bash", "parameters": {"command": "git status", "description": "Shows working tree status"}}]
Notes:
- 120 tool calls per batch
- 125 tool calls per batch
- All calls start in parallel; ordering NOT guaranteed
- Partial failures do not stop other tool calls
- Do NOT use the batch tool within another batch tool.
@@ -3,7 +3,6 @@ import path from "path"
import * as fs from "fs/promises"
import { ApplyPatchTool } from "../../src/tool/apply_patch"
import { Instance } from "../../src/project/instance"
import { FileTime } from "../../src/file/time"
import { tmpdir } from "../fixture/fixture"
const baseCtx = {
@@ -71,8 +70,6 @@ describe("tool.apply_patch freeform", () => {
const deletePath = path.join(fixture.path, "delete.txt")
await fs.writeFile(modifyPath, "line1\nline2\n", "utf-8")
await fs.writeFile(deletePath, "obsolete\n", "utf-8")
FileTime.read(ctx.sessionID, modifyPath)
FileTime.read(ctx.sessionID, deletePath)
const patchText =
"*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch"
@@ -101,7 +98,6 @@ describe("tool.apply_patch freeform", () => {
fn: async () => {
const target = path.join(fixture.path, "multi.txt")
await fs.writeFile(target, "line1\nline2\nline3\nline4\n", "utf-8")
FileTime.read(ctx.sessionID, target)
const patchText =
"*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch"
@@ -122,7 +118,6 @@ describe("tool.apply_patch freeform", () => {
fn: async () => {
const target = path.join(fixture.path, "insert_only.txt")
await fs.writeFile(target, "alpha\nomega\n", "utf-8")
FileTime.read(ctx.sessionID, target)
const patchText = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch"
@@ -142,7 +137,6 @@ describe("tool.apply_patch freeform", () => {
fn: async () => {
const target = path.join(fixture.path, "no_newline.txt")
await fs.writeFile(target, "no newline at end", "utf-8")
FileTime.read(ctx.sessionID, target)
const patchText =
"*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch"
@@ -166,7 +160,6 @@ describe("tool.apply_patch freeform", () => {
const original = path.join(fixture.path, "old", "name.txt")
await fs.mkdir(path.dirname(original), { recursive: true })
await fs.writeFile(original, "old content\n", "utf-8")
FileTime.read(ctx.sessionID, original)
const patchText =
"*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch"
@@ -193,7 +186,6 @@ describe("tool.apply_patch freeform", () => {
await fs.mkdir(path.dirname(destination), { recursive: true })
await fs.writeFile(original, "from\n", "utf-8")
await fs.writeFile(destination, "existing\n", "utf-8")
FileTime.read(ctx.sessionID, original)
const patchText =
"*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch"
@@ -294,7 +286,6 @@ describe("tool.apply_patch freeform", () => {
fn: async () => {
const target = path.join(fixture.path, "modify.txt")
await fs.writeFile(target, "line1\nline2\n", "utf-8")
FileTime.read(ctx.sessionID, target)
const patchText = "*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch"
@@ -331,7 +322,6 @@ describe("tool.apply_patch freeform", () => {
fn: async () => {
const target = path.join(fixture.path, "tail.txt")
await fs.writeFile(target, "alpha\nlast\n", "utf-8")
FileTime.read(ctx.sessionID, target)
const patchText = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch"
@@ -350,7 +340,6 @@ describe("tool.apply_patch freeform", () => {
fn: async () => {
const target = path.join(fixture.path, "two_chunks.txt")
await fs.writeFile(target, "a\nb\nc\nd\n", "utf-8")
FileTime.read(ctx.sessionID, target)
const patchText = "*** Begin Patch\n*** Update File: two_chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch"
@@ -369,7 +358,6 @@ describe("tool.apply_patch freeform", () => {
fn: async () => {
const target = path.join(fixture.path, "multi_ctx.txt")
await fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n", "utf-8")
FileTime.read(ctx.sessionID, target)
const patchText = "*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"
@@ -389,7 +377,6 @@ describe("tool.apply_patch freeform", () => {
const target = path.join(fixture.path, "eof_anchor.txt")
// File has duplicate "marker" lines - one in middle, one at end
await fs.writeFile(target, "start\nmarker\nmiddle\nmarker\nend\n", "utf-8")
FileTime.read(ctx.sessionID, target)
// With EOF anchor, should match the LAST "marker" line, not the first
const patchText =
@@ -454,7 +441,6 @@ EOF`
const target = path.join(fixture.path, "trailing_ws.txt")
// File has trailing spaces on some lines
await fs.writeFile(target, "line1 \nline2\nline3 \n", "utf-8")
FileTime.read(ctx.sessionID, target)
// Patch doesn't have trailing spaces - should still match via rstrip pass
const patchText = "*** Begin Patch\n*** Update File: trailing_ws.txt\n@@\n-line2\n+changed\n*** End Patch"
@@ -475,7 +461,6 @@ EOF`
const target = path.join(fixture.path, "leading_ws.txt")
// File has leading spaces
await fs.writeFile(target, " line1\nline2\n line3\n", "utf-8")
FileTime.read(ctx.sessionID, target)
// Patch without leading spaces - should match via trim pass
const patchText = "*** Begin Patch\n*** Update File: leading_ws.txt\n@@\n-line2\n+changed\n*** End Patch"
@@ -499,7 +484,6 @@ EOF`
const rightQuote = "\u201D"
const emDash = "\u2014"
await fs.writeFile(target, `He said ${leftQuote}hello${rightQuote}\nsome${emDash}dash\nend\n`, "utf-8")
FileTime.read(ctx.sessionID, target)
// Patch uses ASCII equivalents - should match via normalized pass
// The replacement uses ASCII quotes from the patch (not preserving Unicode)
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.1.26",
"version": "1.1.27",
"type": "module",
"license": "MIT",
"scripts": {
@@ -25,4 +25,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.1.26",
"version": "1.1.27",
"type": "module",
"license": "MIT",
"scripts": {
@@ -30,4 +30,4 @@
"publishConfig": {
"directory": "dist"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.1.26",
"version": "1.1.27",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.1.26",
"version": "1.1.27",
"type": "module",
"license": "MIT",
"exports": {
+7 -43
View File
@@ -9,15 +9,11 @@
user-select: none;
cursor: default;
outline: none;
padding: 4px 8px;
white-space: nowrap;
transition-property: background-color, border-color, color, box-shadow;
transition-duration: 200ms;
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
&[data-variant="primary"] {
background-color: var(--icon-strong-base);
border-color: var(--border-weaker-base);
border-color: var(--border-weak-base);
color: var(--icon-invert-base);
[data-slot="icon-svg"] {
@@ -106,15 +102,19 @@
}
&[data-size="small"] {
padding: 2px 10px;
height: 22px;
padding: 0 8px;
&[data-icon] {
padding: 0 12px 0 4px;
}
font-size: var(--font-size-small);
line-height: var(--line-height-large);
gap: 4px;
/* text-12-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large); /* 166.667% */
@@ -163,39 +163,3 @@
outline: none;
}
}
[data-slot="cycle-text"] {
display: inline-flex;
perspective: 400px;
overflow: hidden;
[data-slot="char"] {
display: inline-block;
transform-style: preserve-3d;
transform-origin: 50% 100%;
transform: rotateX(0deg);
opacity: 1;
transition:
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: calc(var(--i, 0) * 40ms);
/* Entry animation using @starting-style */
@starting-style {
transform: rotateX(90deg);
opacity: 0;
}
}
/* Exit animation when animating */
&[data-animating="true"] [data-slot="char"] {
transform: rotateX(-90deg);
opacity: 0;
}
/* Preserve spaces */
[data-slot="space"] {
display: inline-block;
width: 0.25em;
}
}
-1
View File
@@ -109,7 +109,6 @@
flex-direction: column;
flex: 1;
overflow-y: auto;
padding-bottom: 12px;
&:focus-visible {
outline: none;
+22 -27
View File
@@ -2,24 +2,26 @@
[data-component="dropdown-menu-sub-content"] {
min-width: 8rem;
overflow: hidden;
border: none;
border-radius: var(--radius-md);
box-shadow: var(--shadow-xs-border);
border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent);
background-clip: padding-box;
background-color: var(--surface-raised-stronger-non-alpha);
padding: 4px;
z-index: 100;
box-shadow: var(--shadow-md);
z-index: 50;
transform-origin: var(--kb-menu-content-transform-origin);
animation: dropdownMenuContentHide 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
@starting-style {
animation: none;
&:focus,
&:focus-visible {
outline: none;
}
&[data-closed] {
animation: dropdown-menu-close 0.15s ease-out;
}
&[data-expanded] {
pointer-events: auto;
animation: dropdownMenuContentShow 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
animation: dropdown-menu-open 0.15s ease-out;
}
}
@@ -36,23 +38,18 @@
padding: 4px 8px;
border-radius: var(--radius-sm);
cursor: default;
user-select: none;
outline: none;
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-strong);
transition:
background-color 200ms cubic-bezier(0.25, 0, 0.5, 1),
color 200ms cubic-bezier(0.25, 0, 0.5, 1);
outline: none;
user-select: none;
&:hover {
background-color: var(--surface-raised-base-hover);
&[data-highlighted] {
background: var(--surface-raised-base-hover);
}
&[data-disabled] {
@@ -64,8 +61,6 @@
[data-slot="dropdown-menu-sub-trigger"] {
&[data-expanded] {
background: var(--surface-raised-base-hover);
outline: none;
border: none;
}
}
@@ -107,24 +102,24 @@
}
}
@keyframes dropdownMenuContentShow {
@keyframes dropdown-menu-open {
from {
opacity: 0;
transform: scaleY(0.95);
transform: scale(0.96);
}
to {
opacity: 1;
transform: scaleY(1);
transform: scale(1);
}
}
@keyframes dropdownMenuContentHide {
@keyframes dropdown-menu-close {
from {
opacity: 1;
transform: scaleY(1);
transform: scale(1);
}
to {
opacity: 0;
transform: scaleY(0.95);
transform: scale(0.96);
}
}
}
+11 -26
View File
@@ -77,6 +77,7 @@
&[data-variant="ghost"] {
background-color: transparent;
/* color: var(--icon-base); */
[data-slot="icon-svg"] {
color: var(--icon-base);
@@ -84,41 +85,25 @@
&:hover:not(:disabled) {
background-color: var(--surface-raised-base-hover);
/* [data-slot="icon-svg"] { */
/* color: var(--icon-hover); */
/* } */
}
&:focus:not(:disabled) {
background-color: var(--surface-focus);
}
&:active:not(:disabled) {
background-color: var(--surface-raised-base-active);
/* [data-slot="icon-svg"] { */
/* color: var(--icon-active); */
/* } */
}
&:selected:not(:disabled) {
background-color: var(--surface-raised-base-active);
}
}
&[data-variant="weak"] {
opacity: 0.8;
background-color: transparent;
[data-slot="icon-svg"] {
color: var(--icon-base);
}
&:hover:not(:disabled) {
opacity: 1;
background-color: var(--surface-raised-base-hover);
}
&:focus:not(:disabled) {
opacity: 1;
background-color: var(--surface-focus);
}
&:active:not(:disabled) {
opacity: 1;
background-color: var(--surface-raised-base-active);
}
&:selected:not(:disabled) {
opacity: 1;
background-color: var(--surface-raised-base-active);
/* [data-slot="icon-svg"] { */
/* color: var(--icon-selected); */
/* } */
}
&:disabled {
color: var(--icon-invert-base);
+1 -1
View File
@@ -6,7 +6,7 @@ export interface IconButtonProps extends ComponentProps<typeof Kobalte> {
icon: IconProps["name"]
size?: "normal" | "large"
iconSize?: IconProps["size"]
variant?: "primary" | "secondary" | "ghost" | "weak"
variant?: "primary" | "secondary" | "ghost"
}
export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
+3 -3
View File
@@ -8,7 +8,7 @@ const icons = {
"bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
brain: `<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="currentColor"/>`,
"bullet-list": `<path d="M9.58329 13.7497H17.0833M9.58329 6.24967H17.0833M6.24996 6.24967C6.24996 7.17015 5.50377 7.91634 4.58329 7.91634C3.66282 7.91634 2.91663 7.17015 2.91663 6.24967C2.91663 5.3292 3.66282 4.58301 4.58329 4.58301C5.50377 4.58301 6.24996 5.3292 6.24996 6.24967ZM6.24996 13.7497C6.24996 14.6701 5.50377 15.4163 4.58329 15.4163C3.66282 15.4163 2.91663 14.6701 2.91663 13.7497C2.91663 12.8292 3.66282 12.083 4.58329 12.083C5.50377 12.083 6.24996 12.8292 6.24996 13.7497Z" stroke="currentColor" stroke-linecap="square"/>`,
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square" stroke-width="1.25"/>`,
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
@@ -33,7 +33,7 @@ const icons = {
mcp: `<g><path d="M0.972656 9.37176L9.5214 1.60019C10.7018 0.527151 12.6155 0.527151 13.7957 1.60019C14.9761 2.67321 14.9761 4.41295 13.7957 5.48599L7.3397 11.3552" stroke="currentColor" stroke-linecap="round"/><path d="M7.42871 11.2747L13.7957 5.48643C14.9761 4.41338 16.8898 4.41338 18.0702 5.48643L18.1147 5.52688C19.2951 6.59993 19.2951 8.33966 18.1147 9.4127L10.3831 16.4414C9.98966 16.7991 9.98966 17.379 10.3831 17.7366L11.9707 19.1799" stroke="currentColor" stroke-linecap="round"/><path d="M11.6587 3.54346L5.33619 9.29119C4.15584 10.3642 4.15584 12.1039 5.33619 13.177C6.51649 14.25 8.43019 14.25 9.61054 13.177L15.9331 7.42923" stroke="currentColor" stroke-linecap="round"/></g>`,
glasses: `<path d="M0.416626 7.91667H1.66663M19.5833 7.91667H18.3333M11.866 7.57987C11.3165 7.26398 10.6793 7.08333 9.99996 7.08333C9.32061 7.08333 8.68344 7.26398 8.13389 7.57987M8.74996 10C8.74996 12.0711 7.07103 13.75 4.99996 13.75C2.92889 13.75 1.24996 12.0711 1.24996 10C1.24996 7.92893 2.92889 6.25 4.99996 6.25C7.07103 6.25 8.74996 7.92893 8.74996 10ZM18.75 10C18.75 12.0711 17.071 13.75 15 13.75C12.9289 13.75 11.25 12.0711 11.25 10C11.25 7.92893 12.9289 6.25 15 6.25C17.071 6.25 18.75 7.92893 18.75 10Z" stroke="currentColor" stroke-linecap="square"/>`,
"magnifying-glass-menu": `<path d="M2.08325 10.0002H4.58325M2.08325 5.41683H5.41659M2.08325 14.5835H5.41659M16.4583 13.9585L18.7499 16.2502M17.9166 10.0002C17.9166 12.9917 15.4915 15.4168 12.4999 15.4168C9.50838 15.4168 7.08325 12.9917 7.08325 10.0002C7.08325 7.00862 9.50838 4.5835 12.4999 4.5835C15.4915 4.5835 17.9166 7.00862 17.9166 10.0002Z" stroke="currentColor" stroke-linecap="square"/>`,
"window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.25" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
"window-cursor": `<path d="M17.9166 10.4167V3.75H2.08325V17.0833H10.4166M17.9166 13.5897L11.6666 11.6667L13.5897 17.9167L15.032 15.0321L17.9166 13.5897Z" stroke="currentColor" stroke-width="1.07143" stroke-linecap="square"/><path d="M5.00024 6.125C5.29925 6.12518 5.54126 6.36795 5.54126 6.66699C5.54108 6.96589 5.29914 7.20783 5.00024 7.20801C4.7012 7.20801 4.45843 6.966 4.45825 6.66699C4.45825 6.36784 4.70109 6.125 5.00024 6.125ZM7.91626 6.125C8.21541 6.125 8.45825 6.36784 8.45825 6.66699C8.45808 6.966 8.21531 7.20801 7.91626 7.20801C7.61736 7.20783 7.37542 6.96589 7.37524 6.66699C7.37524 6.36795 7.61726 6.12518 7.91626 6.125ZM10.8333 6.125C11.1324 6.125 11.3752 6.36784 11.3752 6.66699C11.3751 6.966 11.1323 7.20801 10.8333 7.20801C10.5342 7.20801 10.2914 6.966 10.2913 6.66699C10.2913 6.36784 10.5341 6.125 10.8333 6.125Z" fill="currentColor" stroke="currentColor" stroke-width="0.25" stroke-linecap="square"/>`,
task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
stop: `<rect x="5" y="5" width="10" height="10" fill="currentColor"/>`,
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
@@ -53,7 +53,7 @@ const icons = {
"dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
"circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
copy: `<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>`,
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square" stroke-width="1.25" />`,
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`,
photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
+5 -2
View File
@@ -19,7 +19,7 @@
[data-component="list"] {
display: flex;
flex-direction: column;
gap: 8px;
gap: 12px;
overflow: hidden;
padding: 0 12px;
@@ -82,9 +82,12 @@
gap: 12px;
overflow-y: auto;
overscroll-behavior: contain;
mask: linear-gradient(to bottom, #ffff calc(100% - var(--bottom-fade)), #0000);
animation: scroll;
animation-timeline: --scroll;
scroll-timeline: --scroll y;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
+3 -10
View File
@@ -3,7 +3,6 @@ import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "
import { createStore } from "solid-js/store"
import { Icon, type IconProps } from "./icon"
import { IconButton } from "./icon-button"
import { ScrollFade } from "./scroll-fade"
import { TextField } from "./text-field"
export interface ListSearchProps {
@@ -198,17 +197,11 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
/>
</div>
<Show when={internalFilter()}>
<IconButton icon="circle-x" variant="weak" onClick={() => setInternalFilter("")} />
<IconButton icon="circle-x" variant="ghost" onClick={() => setInternalFilter("")} />
</Show>
</div>
</Show>
<ScrollFade
ref={setScrollRef}
direction="vertical"
fadeStartSize={0}
fadeEndSize={20}
data-slot="list-scroll"
>
<div ref={setScrollRef} data-slot="list-scroll">
<Show
when={flat().length > 0}
fallback={
@@ -267,7 +260,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
)}
</For>
</Show>
</ScrollFade>
</div>
</div>
)
}
+8 -17
View File
@@ -2,14 +2,13 @@
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-start;
padding: 0;
padding-left: 0;
list-style: none;
&[data-size="normal"] {
width: 240px;
gap: 8px;
gap: 4px;
}
&[data-size="compact"] {
@@ -37,7 +36,6 @@
border: none;
background: none;
padding: 0;
margin: 0;
&[data-active] [data-slot="message-nav-tick-line"] {
background-color: var(--icon-strong-base);
@@ -56,8 +54,7 @@
[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
width: 100%;
color: var(--text-strong);
box-sizing: border-box;
background-color: var(--icon-strong-base);
}
[data-slot="message-nav-message-button"] {
@@ -65,12 +62,11 @@
align-items: center;
align-self: stretch;
width: 100%;
color: inherit;
column-gap: 12px;
cursor: default;
border: none;
background: none;
padding: 0;
padding: 4px 12px;
border-radius: var(--radius-sm);
}
@@ -83,21 +79,16 @@
min-width: 0;
text-align: left;
&:not(:hover) {
color: var(--text-weak);
}
&:hover,
&[data-active] {
color: var(--text-strong);
}
}
[data-slot="message-nav-item"]:hover [data-slot="message-nav-message-button"] {
color: var(--text-strong);
background-color: var(--surface-base);
}
[data-slot="message-nav-item"]:active [data-slot="message-nav-message-button"] {
color: var(--text-base);
background-color: var(--surface-base-active);
}
[data-slot="message-nav-item"]:active [data-slot="message-nav-title-preview"] {
@@ -110,7 +101,7 @@
[data-slot="message-nav-tooltip-content"] {
display: flex;
padding: 4px;
padding: 4px 4px 6px 4px;
justify-content: center;
align-items: center;
border-radius: var(--radius-md);
@@ -128,4 +119,4 @@
* {
margin: 0 !important;
}
}
}
+5 -8
View File
@@ -2,7 +2,6 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
import { ComponentProps, For, Match, Show, splitProps, Switch } from "solid-js"
import { DiffChanges } from "./diff-changes"
import { Tooltip } from "@kobalte/core/tooltip"
import { ScrollReveal } from "./scroll-reveal"
export function MessageNav(
props: ComponentProps<"ul"> & {
@@ -44,16 +43,14 @@ export function MessageNav(
</Match>
<Match when={local.size === "normal"}>
<button data-slot="message-nav-message-button" onClick={handleClick} onKeyDown={handleKeyPress}>
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" class="-ml-1" />
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
<div
data-slot="message-nav-title-preview"
data-active={message.id === local.current?.id || undefined}
>
<ScrollReveal fadeEndSize={12}>
<Show when={local.getLabel?.(message) ?? message.summary?.title} fallback="New message">
{local.getLabel?.(message) ?? message.summary?.title}
</Show>
</ScrollReveal>
<Show when={local.getLabel?.(message) ?? message.summary?.title} fallback="New message">
{local.getLabel?.(message) ?? message.summary?.title}
</Show>
</div>
</button>
</Match>
@@ -82,4 +79,4 @@ export function MessageNav(
<Match when={local.size === "normal"}>{content()}</Match>
</Switch>
)
}
}
+12 -1
View File
@@ -113,11 +113,22 @@
[data-component="reasoning-part"] {
width: 100%;
opacity: 0.5;
color: var(--text-base);
opacity: 0.8;
line-height: var(--line-height-large);
[data-component="markdown"] {
margin-top: 24px;
font-style: italic !important;
p:has(strong) {
margin-top: 24px;
margin-bottom: 0;
&:first-child {
margin-top: 0;
}
}
}
}
+18 -3
View File
@@ -357,6 +357,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
setTimeout(() => setCopied(false), 2000)
}
const toggleExpanded = () => {
if (!canExpand()) return
setExpanded((value) => !value)
}
return (
<div data-component="user-message" data-expanded={expanded()} data-can-expand={canExpand()}>
<Show when={attachments().length > 0}>
@@ -388,19 +393,29 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
</div>
</Show>
<Show when={text()}>
<div data-slot="user-message-text" ref={(el) => (textRef = el)}>
<div data-slot="user-message-text" ref={(el) => (textRef = el)} onClick={toggleExpanded}>
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
<button
data-slot="user-message-expand"
type="button"
aria-label={expanded() ? "Collapse message" : "Expand message"}
onClick={() => setExpanded((v) => !v)}
onClick={(event) => {
event.stopPropagation()
toggleExpanded()
}}
>
<Icon name="chevron-down" size="small" />
</button>
<div data-slot="user-message-copy-wrapper">
<Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
<IconButton icon={copied() ? "check" : "copy"} variant="secondary" onClick={handleCopy} />
<IconButton
icon={copied() ? "check" : "copy"}
variant="secondary"
onClick={(event) => {
event.stopPropagation()
handleCopy()
}}
/>
</Tooltip>
</div>
</div>
-8
View File
@@ -15,14 +15,6 @@
transform-origin: var(--kb-popover-content-transform-origin);
[data-origin-top-right] {
transform-origin: top right;
}
[data-origin-top-left] {
transform-origin: top left;
}
&:focus-within {
outline: none;
}
-123
View File
@@ -1,123 +0,0 @@
[data-component="scroll-fade"] {
overflow: auto;
overscroll-behavior: contain;
scrollbar-width: none;
box-sizing: border-box;
color: inherit;
font: inherit;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
&[data-direction="horizontal"] {
overflow-x: auto;
overflow-y: hidden;
/* Both fades */
&[data-fade-start][data-fade-end] {
mask-image: linear-gradient(
to right,
transparent,
black var(--scroll-fade-start),
black calc(100% - var(--scroll-fade-end)),
transparent
);
-webkit-mask-image: linear-gradient(
to right,
transparent,
black var(--scroll-fade-start),
black calc(100% - var(--scroll-fade-end)),
transparent
);
}
/* Only start fade */
&[data-fade-start]:not([data-fade-end]) {
mask-image: linear-gradient(
to right,
transparent,
black var(--scroll-fade-start),
black 100%
);
-webkit-mask-image: linear-gradient(
to right,
transparent,
black var(--scroll-fade-start),
black 100%
);
}
/* Only end fade */
&:not([data-fade-start])[data-fade-end] {
mask-image: linear-gradient(
to right,
black 0%,
black calc(100% - var(--scroll-fade-end)),
transparent
);
-webkit-mask-image: linear-gradient(
to right,
black 0%,
black calc(100% - var(--scroll-fade-end)),
transparent
);
}
}
&[data-direction="vertical"] {
overflow-y: auto;
overflow-x: hidden;
/* Both fades */
&[data-fade-start][data-fade-end] {
mask-image: linear-gradient(
to bottom,
transparent,
black var(--scroll-fade-start),
black calc(100% - var(--scroll-fade-end)),
transparent
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent,
black var(--scroll-fade-start),
black calc(100% - var(--scroll-fade-end)),
transparent
);
}
/* Only start fade */
&[data-fade-start]:not([data-fade-end]) {
mask-image: linear-gradient(
to bottom,
transparent,
black var(--scroll-fade-start),
black 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent,
black var(--scroll-fade-start),
black 100%
);
}
/* Only end fade */
&:not([data-fade-start])[data-fade-end] {
mask-image: linear-gradient(
to bottom,
black 0%,
black calc(100% - var(--scroll-fade-end)),
transparent
);
-webkit-mask-image: linear-gradient(
to bottom,
black 0%,
black calc(100% - var(--scroll-fade-end)),
transparent
);
}
}
}
-183
View File
@@ -1,183 +0,0 @@
import {
type JSX,
createEffect,
createSignal,
onCleanup,
onMount,
splitProps,
} from "solid-js"
import "./scroll-fade.css"
export interface ScrollFadeProps extends JSX.HTMLAttributes<HTMLDivElement> {
direction?: "horizontal" | "vertical"
fadeStartSize?: number
fadeEndSize?: number
trackTransformSelector?: string
ref?: (el: HTMLDivElement) => void
}
export function ScrollFade(props: ScrollFadeProps) {
const [local, others] = splitProps(props, [
"children",
"direction",
"fadeStartSize",
"fadeEndSize",
"trackTransformSelector",
"class",
"style",
"ref",
])
const direction = () => local.direction ?? "vertical"
const fadeStartSize = () => local.fadeStartSize ?? 20
const fadeEndSize = () => local.fadeEndSize ?? 20
const getTransformOffset = (element: Element): number => {
const style = getComputedStyle(element)
const transform = style.transform
if (!transform || transform === "none") return 0
const match = transform.match(/matrix(?:3d)?\(([^)]+)\)/)
if (!match) return 0
const values = match[1].split(",").map((v) => parseFloat(v.trim()))
const isHorizontal = direction() === "horizontal"
if (transform.startsWith("matrix3d")) {
return isHorizontal ? -(values[12] || 0) : -(values[13] || 0)
} else {
return isHorizontal ? -(values[4] || 0) : -(values[5] || 0)
}
}
let containerRef: HTMLDivElement | undefined
const [fadeStart, setFadeStart] = createSignal(0)
const [fadeEnd, setFadeEnd] = createSignal(0)
const [isScrollable, setIsScrollable] = createSignal(false)
let lastScrollPos = 0
let lastTransformPos = 0
let lastScrollSize = 0
let lastClientSize = 0
const updateFade = () => {
if (!containerRef) return
const isHorizontal = direction() === "horizontal"
const scrollPos = isHorizontal ? containerRef.scrollLeft : containerRef.scrollTop
const scrollSize = isHorizontal ? containerRef.scrollWidth : containerRef.scrollHeight
const clientSize = isHorizontal ? containerRef.clientWidth : containerRef.clientHeight
let transformPos = 0
if (local.trackTransformSelector) {
const transformElement = containerRef.querySelector(local.trackTransformSelector)
if (transformElement) {
transformPos = getTransformOffset(transformElement)
}
}
const effectiveScrollPos = Math.max(scrollPos, transformPos)
if (
effectiveScrollPos === lastScrollPos &&
transformPos === lastTransformPos &&
scrollSize === lastScrollSize &&
clientSize === lastClientSize
) {
return
}
lastScrollPos = effectiveScrollPos
lastTransformPos = transformPos
lastScrollSize = scrollSize
lastClientSize = clientSize
const maxScroll = scrollSize - clientSize
const canScroll = maxScroll > 1
setIsScrollable(canScroll)
if (!canScroll) {
setFadeStart(0)
setFadeEnd(0)
return
}
const progress = maxScroll > 0 ? effectiveScrollPos / maxScroll : 0
const startProgress = Math.min(progress / 0.1, 1)
setFadeStart(startProgress * fadeStartSize())
const endProgress = progress > 0.9 ? (1 - progress) / 0.1 : 1
setFadeEnd(Math.max(0, endProgress) * fadeEndSize())
}
onMount(() => {
if (!containerRef) return
updateFade()
containerRef.addEventListener("scroll", updateFade, { passive: true })
const resizeObserver = new ResizeObserver(() => {
lastScrollSize = 0
lastClientSize = 0
updateFade()
})
resizeObserver.observe(containerRef)
const mutationObserver = new MutationObserver(() => {
lastScrollSize = 0
lastClientSize = 0
requestAnimationFrame(updateFade)
})
mutationObserver.observe(containerRef, {
childList: true,
subtree: true,
characterData: true,
})
let rafId: number
const pollScroll = () => {
updateFade()
rafId = requestAnimationFrame(pollScroll)
}
rafId = requestAnimationFrame(pollScroll)
onCleanup(() => {
containerRef?.removeEventListener("scroll", updateFade)
resizeObserver.disconnect()
mutationObserver.disconnect()
cancelAnimationFrame(rafId)
})
})
createEffect(() => {
local.children
requestAnimationFrame(updateFade)
})
return (
<div
ref={(el) => {
containerRef = el
local.ref?.(el)
}}
data-component="scroll-fade"
data-direction={direction()}
data-scrollable={isScrollable() || undefined}
data-fade-start={fadeStart() > 0 || undefined}
data-fade-end={fadeEnd() > 0 || undefined}
class={local.class}
style={{
...(typeof local.style === "object" ? local.style : {}),
"--scroll-fade-start": `${fadeStart()}px`,
"--scroll-fade-end": `${fadeEnd()}px`,
}}
{...others}
>
{local.children}
</div>
)
}
@@ -1,139 +0,0 @@
import { type JSX, onCleanup, splitProps } from "solid-js"
import { ScrollFade, type ScrollFadeProps } from "./scroll-fade"
const SCROLL_SPEED = 60
const PAUSE_DURATION = 800
interface ScrollAnimationState {
rafId: number | null
startTime: number
running: boolean
}
const startScrollAnimation = (containerEl: HTMLElement): ScrollAnimationState | null => {
containerEl.offsetHeight
const extraWidth = containerEl.scrollWidth - containerEl.clientWidth
if (extraWidth <= 0) return null
const scrollDuration = (extraWidth / SCROLL_SPEED) * 1000
const totalDuration = PAUSE_DURATION + scrollDuration + PAUSE_DURATION + scrollDuration + PAUSE_DURATION
const state: ScrollAnimationState = {
rafId: null,
startTime: performance.now(),
running: true,
}
const animate = (currentTime: number) => {
if (!state.running) return
const elapsed = currentTime - state.startTime
const progress = (elapsed % totalDuration) / totalDuration
const pausePercent = PAUSE_DURATION / totalDuration
const scrollPercent = scrollDuration / totalDuration
const pauseEnd1 = pausePercent
const scrollEnd1 = pauseEnd1 + scrollPercent
const pauseEnd2 = scrollEnd1 + pausePercent
const scrollEnd2 = pauseEnd2 + scrollPercent
let scrollPos = 0
if (progress < pauseEnd1) {
scrollPos = 0
} else if (progress < scrollEnd1) {
const scrollProgress = (progress - pauseEnd1) / scrollPercent
scrollPos = scrollProgress * extraWidth
} else if (progress < pauseEnd2) {
scrollPos = extraWidth
} else if (progress < scrollEnd2) {
const scrollProgress = (progress - pauseEnd2) / scrollPercent
scrollPos = extraWidth * (1 - scrollProgress)
} else {
scrollPos = 0
}
containerEl.scrollLeft = scrollPos
state.rafId = requestAnimationFrame(animate)
}
state.rafId = requestAnimationFrame(animate)
return state
}
const stopScrollAnimation = (state: ScrollAnimationState | null, containerEl?: HTMLElement) => {
if (state) {
state.running = false
if (state.rafId !== null) {
cancelAnimationFrame(state.rafId)
}
}
if (containerEl) {
containerEl.scrollLeft = 0
}
}
export interface ScrollRevealProps extends Omit<ScrollFadeProps, "direction"> {
/** Delay before scroll animation starts on hover (ms). Default: 300 */
hoverDelay?: number
}
export function ScrollReveal(props: ScrollRevealProps) {
const [local, others] = splitProps(props, ["children", "hoverDelay", "ref"])
const hoverDelay = () => local.hoverDelay ?? 300
let containerRef: HTMLDivElement | undefined
let hoverTimeout: ReturnType<typeof setTimeout> | undefined
let scrollAnimationState: ScrollAnimationState | null = null
const handleMouseEnter: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
hoverTimeout = setTimeout(() => {
if (!containerRef) return
containerRef.offsetHeight
const isScrollable = containerRef.scrollWidth > containerRef.clientWidth + 1
if (isScrollable) {
stopScrollAnimation(scrollAnimationState, containerRef)
scrollAnimationState = startScrollAnimation(containerRef)
}
}, hoverDelay())
}
const handleMouseLeave: JSX.EventHandler<HTMLDivElement, MouseEvent> = () => {
if (hoverTimeout) {
clearTimeout(hoverTimeout)
hoverTimeout = undefined
}
stopScrollAnimation(scrollAnimationState, containerRef)
scrollAnimationState = null
}
onCleanup(() => {
if (hoverTimeout) {
clearTimeout(hoverTimeout)
}
stopScrollAnimation(scrollAnimationState, containerRef)
})
return (
<ScrollFade
ref={(el) => {
containerRef = el
local.ref?.(el)
}}
fadeStartSize={8}
fadeEndSize={8}
direction="horizontal"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...others}
>
{local.children}
</ScrollFade>
)
}
+20 -30
View File
@@ -1,11 +1,7 @@
[data-component="select"] {
[data-slot="select-select-trigger"] {
display: flex;
padding: 4px 8px !important;
align-items: center;
justify-content: space-between;
padding: 0 4px 0 8px;
box-shadow: none;
transition: background-color 200ms cubic-bezier(0.25, 0, 0.5, 1);
[data-slot="select-select-trigger-value"] {
overflow: hidden;
@@ -20,10 +16,9 @@
justify-content: center;
flex-shrink: 0;
color: var(--text-weak);
transition: transform 200ms cubic-bezier(0.25, 0, 0.5, 1);
transition: transform 0.1s ease-in-out;
}
&:hover,
&[data-expanded] {
&[data-variant="secondary"] {
background-color: var(--button-secondary-hover);
@@ -51,26 +46,21 @@
}
[data-component="select-content"] {
min-width: 8rem;
min-width: 4rem;
max-width: 23rem;
overflow: hidden;
border-radius: var(--radius-md);
background-color: var(--surface-raised-stronger-non-alpha);
padding: 4px;
padding: 2px;
box-shadow: var(--shadow-xs-border);
z-index: 50;
transform-origin: var(--kb-popper-content-transform-origin);
pointer-events: none;
animation: selectContentHide 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
@starting-style {
animation: none;
&[data-closed] {
animation: select-close 0.15s ease-out;
}
&[data-expanded] {
pointer-events: auto;
animation: selectContentShow 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
animation: select-open 0.15s ease-out;
}
[data-slot="select-select-content-list"] {
@@ -97,13 +87,13 @@
position: relative;
display: flex;
align-items: center;
padding: 4px 8px;
padding: 0 6px 0 6px;
gap: 12px;
border-radius: var(--radius-sm);
/* text-12-medium */
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large); /* 166.667% */
@@ -112,13 +102,13 @@
color: var(--text-strong);
transition:
background-color 200ms cubic-bezier(0.25, 0, 0.5, 1),
color 200ms cubic-bezier(0.25, 0, 0.5, 1);
background-color 0.2s ease-in-out,
color 0.2s ease-in-out;
outline: none;
user-select: none;
&:hover {
background-color: var(--surface-raised-base-hover);
&[data-highlighted] {
background: var(--surface-raised-base-hover);
}
&[data-disabled] {
background-color: var(--surface-raised-base);
@@ -141,24 +131,24 @@
}
}
@keyframes selectContentShow {
@keyframes select-open {
from {
opacity: 0;
transform: scaleY(0.95);
transform: scale(0.95);
}
to {
opacity: 1;
transform: scaleY(1);
transform: scale(1);
}
}
@keyframes selectContentHide {
@keyframes select-close {
from {
opacity: 1;
transform: scaleY(1);
transform: scale(1);
}
to {
opacity: 0;
transform: scaleY(0.95);
transform: scale(0.95);
}
}
}
+1 -2
View File
@@ -49,7 +49,6 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
placement="bottom-start"
value={local.current}
options={grouped()}
gutter={12}
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
optionTextValue={(x) => (local.label ? local.label(x) : (x as string))}
optionGroupChildren="options"
@@ -74,7 +73,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
: (itemProps.item.rawValue as string)}
</Kobalte.ItemLabel>
<Kobalte.ItemIndicator data-slot="select-select-item-indicator">
<Icon name="check" size="small" class="text-icon-strong-base" />
<Icon name="check-small" size="small" />
</Kobalte.ItemIndicator>
</Kobalte.Item>
)}
@@ -72,17 +72,17 @@ export const SessionReview = (props: SessionReviewProps) => {
<div data-slot="session-review-actions">
<Show when={props.onDiffStyleChange}>
<RadioGroup
options={["unified", "split"]}
options={["unified", "split"] as const}
current={diffStyle()}
value={(style) => style}
label={(style) => (style === "unified" ? "Unified" : "Split")}
onSelect={(style) => style && props.onDiffStyleChange?.(style as SessionReviewDiffStyle)}
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
/>
</Show>
<Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
<Switch>
<Match when={open().length > 0}>Collapse</Match>
<Match when={true}>Expand</Match>
<Match when={open().length > 0}>Collapse all</Match>
<Match when={true}>Expand all</Match>
</Switch>
</Button>
{props.actions}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.1.26",
"version": "1.1.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.1.26",
"version": "1.1.27",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.1.26",
"version": "1.1.27",
"publisher": "sst-dev",
"repository": {
"type": "git",