Compare commits

...

21 Commits

Author SHA1 Message Date
opencode effa7b45cf release: v1.0.202 2025-12-26 02:11:47 +00:00
Adam b307075063 chore: brain icon 2025-12-25 20:06:41 -06:00
Adam aaf9a5d434 fix(desktop): user message display 2025-12-25 19:45:20 -06:00
Adam e9c2f1f3f3 fix(desktop): padding 2025-12-25 19:22:16 -06:00
Adam 7469cba7cf fix(desktop): move session context to top-right 2025-12-25 19:21:04 -06:00
Adam 5420702f69 fix(desktop): missing keybinds in tooltips 2025-12-25 19:07:42 -06:00
Adam 583751ecae fix(desktop): markdown rendering perf 2025-12-25 19:07:42 -06:00
GitHub Action d0a1b5ef96 chore: generate 2025-12-26 01:03:22 +00:00
Adam 42f2bc7199 fix(desktop): can't collapse project with active session 2025-12-25 19:02:43 -06:00
Adam 603dae562a chore(ui): radio group primitive 2025-12-25 18:46:57 -06:00
Adam 650bd76370 feat(desktop): better indicator that session is busy 2025-12-25 14:31:10 -06:00
opencode 8aa3520683 release: v1.0.201 2025-12-25 14:07:19 +00:00
Adam 5b5b8c57d9 fix(desktop): so many prompt input fixes, merry christmas 2025-12-25 08:04:42 -06:00
GitHub Action f057b22e20 ignore: update download stats 2025-12-25 2025-12-25 12:04:54 +00:00
GitHub Action 388d40e41f chore: generate 2025-12-25 05:59:27 +00:00
Dax Raad f397c92ddf remove list tool 2025-12-25 00:58:47 -05:00
opencode 6f9bea4e1f release: v1.0.200 2025-12-25 04:39:32 +00:00
Adam 5c49b4cbfc fix(desktop): scroll jank in session turn and review 2025-12-24 22:36:45 -06:00
GitHub Action b746e831e2 chore: generate 2025-12-25 04:17:39 +00:00
Adam 2178deef91 fix(desktop): override agent model 2025-12-24 22:16:58 -06:00
Adam b1d2fb5319 fix(desktop): reconcile session diff updates 2025-12-24 22:16:44 -06:00
33 changed files with 482 additions and 218 deletions
+1
View File
@@ -180,3 +180,4 @@
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
+15 -15
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.0.199",
"version": "1.0.202",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -77,7 +77,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.199",
"version": "1.0.202",
"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.0.199",
"version": "1.0.202",
"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.0.199",
"version": "1.0.202",
"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.0.199",
"version": "1.0.202",
"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.0.199",
"version": "1.0.202",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@solid-primitives/storage": "catalog:",
@@ -207,7 +207,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.199",
"version": "1.0.202",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -236,7 +236,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.199",
"version": "1.0.202",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -252,7 +252,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.199",
"version": "1.0.202",
"bin": {
"opencode": "./bin/opencode",
},
@@ -346,7 +346,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.199",
"version": "1.0.202",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -366,7 +366,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.199",
"version": "1.0.202",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -377,7 +377,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.199",
"version": "1.0.202",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -390,7 +390,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.199",
"version": "1.0.202",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -425,7 +425,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.199",
"version": "1.0.202",
"dependencies": {
"zod": "catalog:",
},
@@ -436,7 +436,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.199",
"version": "1.0.202",
"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.0.199",
"version": "1.0.202",
"description": "",
"type": "module",
"exports": {
+89 -65
View File
@@ -103,7 +103,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttachments: ImageAttachmentPart[]
mode: "normal" | "shell"
applyingHistory: boolean
userHasEdited: boolean
}>({
popover: null,
historyIndex: -1,
@@ -113,7 +112,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttachments: [],
mode: "normal",
applyingHistory: false,
userHasEdited: false,
})
const MAX_HISTORY = 100
@@ -150,7 +148,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
const length = position === "start" ? 0 : promptLength(p)
setStore("applyingHistory", true)
setStore("userHasEdited", false)
prompt.set(p, length)
requestAnimationFrame(() => {
editorRef.focus()
@@ -279,11 +276,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
createEffect(() => {
if (isFocused()) {
handleInput()
} else {
setStore("popover", null)
}
if (!isFocused()) setStore("popover", null)
})
const handleFileSelect = (path: string | undefined) => {
@@ -363,7 +356,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
() => prompt.current(),
(currentParts) => {
const domParts = parseFromDOM()
if (isPromptEqual(currentParts, domParts)) return
const normalized = Array.from(editorRef.childNodes).every((node) => {
if (node.nodeType === Node.TEXT_NODE) return true
if (node.nodeType !== Node.ELEMENT_NODE) return false
return (node as HTMLElement).dataset.type === "file"
})
if (normalized && isPromptEqual(currentParts, domParts)) return
const selection = window.getSelection()
let cursorPosition: number | null = null
@@ -395,34 +393,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
)
const parseFromDOM = (): Prompt => {
const newParts: Prompt = []
const parts: Prompt = []
let position = 0
let buffer = ""
const pushText = (content: string) => {
const flushText = () => {
const content = buffer.replace(/\r\n?/g, "\n")
buffer = ""
if (!content) return
newParts.push({ type: "text", content, start: position, end: position + content.length })
parts.push({ type: "text", content, start: position, end: position + content.length })
position += content.length
}
const rangeText = (range: Range) => {
const fragment = range.cloneContents()
const container = document.createElement("div")
container.append(fragment)
return container.innerText
}
const files = Array.from(editorRef.querySelectorAll<HTMLElement>("[data-type=file]"))
let last: HTMLElement | undefined
files.forEach((file) => {
const before = document.createRange()
before.selectNodeContents(editorRef)
if (last) before.setStartAfter(last)
before.setEndBefore(file)
pushText(rangeText(before))
const pushFile = (file: HTMLElement) => {
const content = file.textContent ?? ""
newParts.push({
parts.push({
type: "file",
path: file.dataset.path!,
content,
@@ -430,16 +415,44 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
end: position + content.length,
})
position += content.length
last = file
}
const visit = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
buffer += node.textContent ?? ""
return
}
if (node.nodeType !== Node.ELEMENT_NODE) return
const el = node as HTMLElement
if (el.dataset.type === "file") {
flushText()
pushFile(el)
return
}
if (el.tagName === "BR") {
buffer += "\n"
return
}
for (const child of Array.from(el.childNodes)) {
visit(child)
}
}
const children = Array.from(editorRef.childNodes)
children.forEach((child, index) => {
const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName)
visit(child)
if (isBlock && index < children.length - 1) {
buffer += "\n"
}
})
const after = document.createRange()
after.selectNodeContents(editorRef)
if (last) after.setStartAfter(last)
pushText(rangeText(after))
flushText()
if (newParts.length === 0) newParts.push(...DEFAULT_PROMPT)
return newParts
if (parts.length === 0) parts.push(...DEFAULT_PROMPT)
return parts
}
const handleInput = () => {
@@ -452,7 +465,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (shouldReset) {
setStore("popover", null)
setStore("userHasEdited", false)
if (store.historyIndex >= 0 && !store.applyingHistory) {
setStore("historyIndex", -1)
setStore("savedPrompt", null)
@@ -487,10 +499,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("savedPrompt", null)
}
if (!store.applyingHistory) {
setStore("userHasEdited", true)
}
prompt.set(rawParts, cursorPosition)
}
@@ -516,27 +524,39 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const gap = document.createTextNode(" ")
const range = selection.getRangeAt(0)
if (atMatch) {
let runningLength = 0
const setEdge = (edge: "start" | "end", offset: number) => {
let remaining = offset
const nodes = Array.from(editorRef.childNodes)
const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)
let currentNode = walker.nextNode()
while (currentNode) {
const textContent = currentNode.textContent || ""
if (runningLength + textContent.length >= atMatch.index!) {
const localStart = atMatch.index! - runningLength
const localEnd = cursorPosition - runningLength
if (currentNode === range.startContainer || runningLength + textContent.length >= cursorPosition) {
range.setStart(currentNode, localStart)
range.setEnd(currentNode, Math.min(localEnd, textContent.length))
break
}
for (const node of nodes) {
const length = node.textContent?.length ?? 0
const isText = node.nodeType === Node.TEXT_NODE
const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
if (isText && remaining <= length) {
if (edge === "start") range.setStart(node, remaining)
if (edge === "end") range.setEnd(node, remaining)
return
}
runningLength += textContent.length
currentNode = walker.nextNode()
if (isFile && remaining <= length) {
if (edge === "start" && remaining === 0) range.setStartBefore(node)
if (edge === "start" && remaining > 0) range.setStartAfter(node)
if (edge === "end" && remaining === 0) range.setEndBefore(node)
if (edge === "end" && remaining > 0) range.setEndAfter(node)
return
}
remaining -= length
}
}
if (atMatch) {
const start = atMatch.index ?? cursorPosition - atMatch[0].length
setEdge("start", start)
setEdge("end", cursorPosition)
}
range.deleteContents()
range.insertNode(gap)
range.insertNode(pill)
@@ -584,8 +604,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const navigateHistory = (direction: "up" | "down") => {
if (store.userHasEdited) return false
const entries = store.mode === "shell" ? shellHistory.entries : history.entries
const current = store.historyIndex
@@ -692,6 +710,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return
}
if (event.key === "Enter" && event.shiftKey) {
addPart({ type: "text", content: "\n", start: 0, end: 0 })
event.preventDefault()
return
}
if (event.key === "Enter" && !event.shiftKey) {
handleSubmit(event)
}
@@ -717,7 +740,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
addToHistory(currentPrompt, store.mode)
setStore("historyIndex", -1)
setStore("savedPrompt", null)
setStore("userHasEdited", false)
let existing = info()
if (!existing) {
@@ -967,18 +989,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onInput={handleInput}
onKeyDown={handleKeyDown}
classList={{
"w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"[&>[data-type=file]]:text-icon-info-active": true,
"w-full px-5 py-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"[&_[data-type=file]]:text-icon-info-active": true,
"font-mono!": store.mode === "shell",
}}
/>
<Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
<div class="absolute top-0 inset-x-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
<div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
{store.mode === "shell"
? "Enter shell command..."
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
</div>
</Show>
<div class="absolute top-4.5 right-4">
<SessionContextUsage />
</div>
</div>
<div class="relative p-3 flex items-center justify-between">
<div class="flex items-center justify-start gap-1">
@@ -1035,7 +1060,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Tooltip>
</Match>
</Switch>
<SessionContextUsage />
</div>
<div class="flex items-center gap-1 absolute right-2 bottom-2">
<input
@@ -34,28 +34,27 @@ export function SessionContextUsage() {
<Show when={context?.()}>
{(ctx) => (
<Tooltip
openDelay={300}
value={
<div class="flex flex-col gap-1 p-2">
<div class="flex justify-between gap-4">
<span class="text-text-weaker">Tokens</span>
<span class="text-text-strong">{ctx().tokens}</span>
<div class="flex flex-col gap-1">
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Tokens</span>
<span class="text-left flex-1">{ctx().tokens}</span>
</div>
<div class="flex justify-between gap-4">
<span class="text-text-weaker">Usage</span>
<span class="text-text-strong">{ctx().percentage ?? 0}%</span>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Usage</span>
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
</div>
<div class="flex justify-between gap-4">
<span class="text-text-weaker">Cost</span>
<span class="text-text-strong">{cost()}</span>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Cost</span>
<span class="text-left flex-1">{cost()}</span>
</div>
</div>
}
placement="top"
>
<div class="flex items-center gap-1">
<span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span>
<div class="flex items-center gap-1.5">
<ProgressCircle size={16} strokeWidth={2} percentage={ctx().percentage ?? 0} />
{/* <span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span> */}
</div>
</Tooltip>
)}
+3 -3
View File
@@ -215,13 +215,13 @@ function createGlobalSync() {
break
}
case "session.diff":
setStore("session_diff", event.properties.sessionID, event.properties.diff)
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
break
case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos))
break
case "session.status": {
setStore("session_status", event.properties.sessionID, event.properties.status)
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
break
}
case "message.updated": {
+1 -19
View File
@@ -1,5 +1,5 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo } from "solid-js"
import { batch, createMemo } from "solid-js"
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -62,24 +62,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
// Automatically update model when agent changes
createEffect(() => {
const value = agent.current()
if (value.model) {
if (isModelValid(value.model))
model.set({
providerID: value.model.providerID,
modelID: value.model.modelID,
})
// else
// toast.show({
// type: "warning",
// message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
// duration: 3000,
// })
}
})
const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [store, setStore] = createStore<{
+26 -4
View File
@@ -9,6 +9,7 @@ import {
ParentProps,
Show,
Switch,
untrack,
type JSX,
} from "solid-js"
import { DateTime } from "luxon"
@@ -323,7 +324,7 @@ export default function Layout(props: ParentProps) {
const id = params.id
setStore("lastSession", directory, id)
notification.session.markViewed(id)
layout.projects.expand(directory)
untrack(() => layout.projects.expand(directory))
requestAnimationFrame(() => scrollToSession(id))
})
@@ -472,7 +473,12 @@ export default function Layout(props: ParentProps) {
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
>
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
<span
classList={{
"text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
"animate-pulse": isWorking(),
}}
>
{props.session.title}
</span>
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
@@ -513,7 +519,15 @@ export default function Layout(props: ParentProps) {
</A>
</Tooltip>
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
<Tooltip placement={props.mobile ? "bottom" : "right"} value="Archive session">
<Tooltip
placement={props.mobile ? "bottom" : "right"}
value={
<div class="flex items-center gap-2">
<span>Archive session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.archive")}</span>
</div>
}
>
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
</Tooltip>
</div>
@@ -578,7 +592,15 @@ export default function Layout(props: ParentProps) {
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Tooltip placement="top" value="New session">
<Tooltip
placement="top"
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
</Tooltip>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.199",
"version": "1.0.202",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.199",
"version": "1.0.202",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.199",
"version": "1.0.202",
"$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.0.199",
"version": "1.0.202",
"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.0.199",
"version": "1.0.202",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.199",
"version": "1.0.202",
"private": true,
"type": "module",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.199"
version = "1.0.202"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.199/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.202/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.199",
"version": "1.0.202",
"$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.0.199",
"version": "1.0.202",
"name": "opencode",
"type": "module",
"private": true,
-1
View File
@@ -95,7 +95,6 @@ export namespace ToolRegistry {
ReadTool,
GlobTool,
GrepTool,
ListTool,
EditTool,
WriteTool,
TaskTool,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.199",
"version": "1.0.202",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.199",
"version": "1.0.202",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.199",
"version": "1.0.202",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.199",
"version": "1.0.202",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
+1
View File
@@ -6,6 +6,7 @@ const icons = {
"arrow-left": `<path d="M8.33464 4.58398L2.91797 10.0007L8.33464 15.4173M3.33464 10.0007H17.0846" stroke="currentColor" stroke-linecap="square"/>`,
archive: `<path d="M16.8747 6.24935H17.3747V5.74935H16.8747V6.24935ZM16.8747 16.8743V17.3743H17.3747V16.8743H16.8747ZM3.12467 16.8743H2.62467V17.3743H3.12467V16.8743ZM3.12467 6.24935V5.74935H2.62467V6.24935H3.12467ZM2.08301 2.91602V2.41602H1.58301V2.91602H2.08301ZM17.9163 2.91602H18.4163V2.41602H17.9163V2.91602ZM17.9163 6.24935V6.74935H18.4163V6.24935H17.9163ZM2.08301 6.24935H1.58301V6.74935H2.08301V6.24935ZM8.33301 9.08268H7.83301V10.0827H8.33301V9.58268V9.08268ZM11.6663 10.0827H12.1663V9.08268H11.6663V9.58268V10.0827ZM16.8747 6.24935H16.3747V16.8743H16.8747H17.3747V6.24935H16.8747ZM16.8747 16.8743V16.3743H3.12467V16.8743V17.3743H16.8747V16.8743ZM3.12467 16.8743H3.62467V6.24935H3.12467H2.62467V16.8743H3.12467ZM3.12467 6.24935V6.74935H16.8747V6.24935V5.74935H3.12467V6.24935ZM2.08301 2.91602V3.41602H17.9163V2.91602V2.41602H2.08301V2.91602ZM17.9163 2.91602H17.4163V6.24935H17.9163H18.4163V2.91602H17.9163ZM17.9163 6.24935V5.74935H2.08301V6.24935V6.74935H17.9163V6.24935ZM2.08301 6.24935H2.58301V2.91602H2.08301H1.58301V6.24935H2.08301ZM8.33301 9.58268V10.0827H11.6663V9.58268V9.08268H8.33301V9.58268Z" fill="currentColor"/>`,
"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"/>`,
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
+1 -16
View File
@@ -1,21 +1,6 @@
import { useMarked } from "../context/marked"
import { ComponentProps, createResource, splitProps } from "solid-js"
function strip(text: string): string {
const trimmed = text.trim()
const match = trimmed.match(/^<([A-Za-z]\w*)>/)
if (!match) return text
const tagName = match[1]
const closingTag = `</${tagName}>`
if (trimmed.endsWith(closingTag)) {
const content = trimmed.slice(match[0].length, -closingTag.length)
return content.trim()
}
return text
}
export function Markdown(
props: ComponentProps<"div"> & {
text: string
@@ -26,7 +11,7 @@ export function Markdown(
const [local, others] = splitProps(props, ["text", "class", "classList"])
const marked = useMarked()
const [html] = createResource(
() => strip(local.text),
() => local.text,
async (markdown) => {
return marked.parse(markdown)
},
+1 -5
View File
@@ -72,14 +72,10 @@
}
[data-slot="user-message-text"] {
display: -webkit-box;
white-space: pre-wrap;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
background: var(--surface-inset-base);
padding: 2px 6px;
padding: 6px 12px;
border-radius: 4px;
border: 0.5px solid var(--border-weak-base);
}
+160
View File
@@ -0,0 +1,160 @@
[data-component="radio-group"] {
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 2);
[data-slot="radio-group-wrapper"] {
all: unset;
background-color: var(--surface-base);
border-radius: var(--radius-md);
box-shadow: inset 0 0 0 1px var(--border-weak-base);
margin: 0;
padding: 0;
position: relative;
width: fit-content;
}
[data-slot="radio-group-items"] {
display: inline-flex;
list-style: none;
flex-direction: row;
}
[data-slot="radio-group-indicator"] {
background: var(--button-secondary-base);
border-radius: var(--radius-md);
box-shadow:
var(--shadow-xs),
inset 0 0 0 var(--indicator-focus-width, 0px) var(--border-selected),
inset 0 0 0 1px var(--border-base);
content: "";
opacity: var(--indicator-opacity, 1);
position: absolute;
transition:
opacity 300ms ease-in-out,
box-shadow 100ms ease-in-out,
width 150ms ease,
height 150ms ease,
transform 150ms ease;
}
[data-slot="radio-group-item"] {
position: relative;
}
/* Separator between items */
[data-slot="radio-group-item"]:not(:first-of-type)::before {
background: var(--border-weak-base);
border-radius: var(--radius-xs);
content: "";
inset: 6px 0;
position: absolute;
transition: opacity 150ms ease;
width: 1px;
transform: translateX(-0.5px);
}
/* Hide separator when item or previous item is checked */
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])::before,
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])
+ [data-slot="radio-group-item"]::before {
opacity: 0;
}
[data-slot="radio-group-item-label"] {
color: var(--text-weak);
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
border-radius: var(--radius-md);
cursor: pointer;
display: flex;
flex-wrap: nowrap;
gap: calc(var(--spacing) * 1);
line-height: 1;
padding: 6px 12px;
place-content: center;
position: relative;
transition-duration: 150ms;
transition-property: color, opacity;
transition-timing-function: ease-in-out;
user-select: none;
}
[data-slot="radio-group-item-input"] {
all: unset;
}
/* Checked state */
[data-slot="radio-group-item-input"][data-checked] + [data-slot="radio-group-item-label"] {
color: var(--text-strong);
}
/* Disabled state */
[data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] {
cursor: not-allowed;
opacity: 0.5;
}
/* Hover state for unchecked, enabled items */
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + [data-slot="radio-group-item-label"] {
cursor: pointer;
user-select: none;
}
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
+ [data-slot="radio-group-item-label"]:hover {
color: var(--text-base);
}
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
+ [data-slot="radio-group-item-label"]:active {
opacity: 0.7;
}
/* Focus state */
[data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible)
[data-slot="radio-group-indicator"] {
--indicator-focus-width: 2px;
}
/* Hide indicator when nothing is checked */
[data-slot="radio-group-wrapper"]:not(:has([data-slot="radio-group-item-input"][data-checked]))
[data-slot="radio-group-indicator"] {
--indicator-opacity: 0;
}
/* Vertical orientation */
&[aria-orientation="vertical"] [data-slot="radio-group-items"] {
flex-direction: column;
}
&[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
height: 1px;
width: auto;
inset: 0 6px;
transform: translateY(-0.5px);
}
/* Small size variant */
&[data-size="small"] {
[data-slot="radio-group-item-label"] {
font-size: 12px;
padding: 4px 8px;
}
[data-slot="radio-group-item"]:not(:first-of-type)::before {
inset: 4px 0;
}
&[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
inset: 0 4px;
}
}
/* Disabled root state */
&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
@@ -0,0 +1,75 @@
import { SegmentedControl as Kobalte } from "@kobalte/core/segmented-control"
import { For, splitProps } from "solid-js"
import type { ComponentProps, JSX } from "solid-js"
export type RadioGroupProps<T> = Omit<
ComponentProps<typeof Kobalte>,
"value" | "defaultValue" | "onChange" | "children"
> & {
options: T[]
current?: T
defaultValue?: T
value?: (x: T) => string
label?: (x: T) => JSX.Element | string
onSelect?: (value: T | undefined) => void
class?: ComponentProps<"div">["class"]
classList?: ComponentProps<"div">["classList"]
size?: "small" | "medium"
}
export function RadioGroup<T>(props: RadioGroupProps<T>) {
const [local, others] = splitProps(props, [
"class",
"classList",
"options",
"current",
"defaultValue",
"value",
"label",
"onSelect",
"size",
])
const getValue = (item: T): string => {
if (local.value) return local.value(item)
return String(item)
}
const getLabel = (item: T): JSX.Element | string => {
if (local.label) return local.label(item)
return String(item)
}
const findOption = (v: string): T | undefined => {
return local.options.find((opt) => getValue(opt) === v)
}
return (
<Kobalte
{...others}
data-component="radio-group"
data-size={local.size ?? "medium"}
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
value={local.current ? getValue(local.current) : undefined}
defaultValue={local.defaultValue ? getValue(local.defaultValue) : undefined}
onChange={(v) => local.onSelect?.(findOption(v))}
>
<div role="presentation" data-slot="radio-group-wrapper">
<Kobalte.Indicator data-slot="radio-group-indicator" />
<div role="presentation" data-slot="radio-group-items">
<For each={local.options}>
{(option) => (
<Kobalte.Item value={getValue(option)} data-slot="radio-group-item">
<Kobalte.ItemInput data-slot="radio-group-item-input" />
<Kobalte.ItemLabel data-slot="radio-group-item-label">{getLabel(option)}</Kobalte.ItemLabel>
</Kobalte.Item>
)}
</For>
</div>
</div>
</Kobalte>
)
}
+1 -1
View File
@@ -26,7 +26,7 @@
align-items: flex-start;
align-self: stretch;
min-width: 0;
gap: 42px;
gap: 28px;
overflow-anchor: none;
}
+73 -54
View File
@@ -124,77 +124,96 @@ export function SessionTurn(
return allMessages().filter((m) => m.role === "assistant" && m.parentID === msg.id) as AssistantMessage[]
})
const assistantParts = createMemo(() => {
const result: PartType[] = []
for (const m of assistantMessages()) {
const msgParts = data.store.part[m.id]
if (msgParts) {
for (const p of msgParts) {
if (p) result.push(p)
}
}
}
return result
})
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
const error = createMemo(() => assistantMessages().find((m) => m.error)?.error)
const lastTextPart = createMemo(() => {
const ap = assistantParts()
for (let i = ap.length - 1; i >= 0; i--) {
if (ap[i]?.type === "text") return ap[i]
const msgs = assistantMessages()
for (let mi = msgs.length - 1; mi >= 0; mi--) {
const msgParts = data.store.part[msgs[mi].id] ?? []
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (part?.type === "text") return part as TextPart
}
}
return undefined
})
const hasSteps = createMemo(() => assistantParts().some((p) => p?.type === "tool"))
const isShellMode = createMemo(() => {
const p = parts()
const ap = assistantParts()
if (p.every((part) => part?.type === "text" && part?.synthetic) && ap.length === 1) {
const assistantPart = ap[0]
if (assistantPart?.type === "tool" && assistantPart?.tool === "bash") return true
const hasSteps = createMemo(() => {
for (const m of assistantMessages()) {
const msgParts = data.store.part[m.id]
if (!msgParts) continue
for (const p of msgParts) {
if (p?.type === "tool") return true
}
}
return false
})
const rawStatus = createMemo(() => {
const ap = assistantParts()
const currentTask = ap.findLast(
(p) =>
p &&
p.type === "tool" &&
p.tool === "task" &&
p.state &&
"metadata" in p.state &&
p.state.metadata &&
p.state.metadata.sessionId &&
p.state.status === "running",
) as ToolPart | undefined
const shellModePart = createMemo(() => {
const p = parts()
if (!p.every((part) => part?.type === "text" && part?.synthetic)) return
let resolvedParts = ap
if (currentTask?.state && "metadata" in currentTask.state && currentTask.state.metadata?.sessionId) {
const taskMessages = data.store.message[currentTask.state.metadata.sessionId as string]?.filter(
(m) => m.role === "assistant",
)
if (taskMessages) {
const taskParts: PartType[] = []
for (const m of taskMessages) {
const msgParts = data.store.part[m.id]
if (msgParts) {
for (const p of msgParts) {
if (p) taskParts.push(p)
}
}
const msgs = assistantMessages()
if (msgs.length !== 1) return
const msgParts = data.store.part[msgs[0].id] ?? []
if (msgParts.length !== 1) return
const assistantPart = msgParts[0]
if (assistantPart?.type === "tool" && assistantPart.tool === "bash") return assistantPart
})
const isShellMode = createMemo(() => !!shellModePart())
const rawStatus = createMemo(() => {
const msgs = assistantMessages()
let last: PartType | undefined
let currentTask: ToolPart | undefined
for (let mi = msgs.length - 1; mi >= 0; mi--) {
const msgParts = data.store.part[msgs[mi].id] ?? []
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (!part) continue
if (!last) last = part
if (
part.type === "tool" &&
part.tool === "task" &&
part.state &&
"metadata" in part.state &&
part.state.metadata?.sessionId &&
part.state.status === "running"
) {
currentTask = part as ToolPart
break
}
}
if (currentTask) break
}
const taskSessionId =
currentTask?.state && "metadata" in currentTask.state
? (currentTask.state.metadata?.sessionId as string | undefined)
: undefined
if (taskSessionId) {
const taskMessages = data.store.message[taskSessionId] ?? []
for (let mi = taskMessages.length - 1; mi >= 0; mi--) {
const msg = taskMessages[mi]
if (!msg || msg.role !== "assistant") continue
const msgParts = data.store.part[msg.id] ?? []
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
const part = msgParts[pi]
if (part) return computeStatusFromPart(part)
}
if (taskParts.length > 0) resolvedParts = taskParts
}
}
return computeStatusFromPart(resolvedParts.at(-1))
return computeStatusFromPart(last)
})
const status = createMemo(
@@ -367,7 +386,7 @@ export function SessionTurn(
>
<Switch>
<Match when={isShellMode()}>
<Part part={assistantParts()[0]} message={msg()} defaultOpen />
<Part part={shellModePart()!} message={msg()} defaultOpen />
</Match>
<Match when={true}>
{/* Title (sticky) */}
+1
View File
@@ -29,6 +29,7 @@
@import "../components/message-nav.css" layer(components);
@import "../components/popover.css" layer(components);
@import "../components/progress-circle.css" layer(components);
@import "../components/radio-group.css" layer(components);
@import "../components/resize-handle.css" layer(components);
@import "../components/select.css" layer(components);
@import "../components/spinner.css" layer(components);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.199",
"version": "1.0.202",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.199",
"version": "1.0.202",
"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.0.199",
"version": "1.0.202",
"publisher": "sst-dev",
"repository": {
"type": "git",