diff --git a/bun.lock b/bun.lock index e5892a7745..53d80630b8 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -71,7 +71,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.25", + "version": "1.1.26", "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.25", + "version": "1.1.26", "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.25", + "version": "1.1.26", "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.25", + "version": "1.1.26", "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.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -209,7 +209,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -238,7 +238,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -254,7 +254,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.25", + "version": "1.1.26", "bin": { "opencode": "./bin/opencode", }, @@ -358,7 +358,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -378,7 +378,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.25", + "version": "1.1.26", "devDependencies": { "@hey-api/openapi-ts": "0.90.4", "@tsconfig/node22": "catalog:", @@ -389,7 +389,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -402,7 +402,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -443,7 +443,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "zod": "catalog:", }, @@ -454,7 +454,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/index.html b/packages/app/index.html index 450807a42e..1e516cbbb1 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -4,10 +4,10 @@ OpenCode - - - - + + + + diff --git a/packages/app/package.json b/packages/app/package.json index 2a754c9673..736a262504 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.25", + "version": "1.1.26", "description": "", "type": "module", "exports": { diff --git a/packages/app/public/apple-touch-icon-v2.png b/packages/app/public/apple-touch-icon-v2.png new file mode 120000 index 0000000000..c0d4353db4 --- /dev/null +++ b/packages/app/public/apple-touch-icon-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/apple-touch-icon-v2.png \ No newline at end of file diff --git a/packages/app/public/favicon-96x96-v2.png b/packages/app/public/favicon-96x96-v2.png new file mode 120000 index 0000000000..b3129f6bf9 --- /dev/null +++ b/packages/app/public/favicon-96x96-v2.png @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-96x96-v2.png \ No newline at end of file diff --git a/packages/app/public/favicon-v2.ico b/packages/app/public/favicon-v2.ico new file mode 120000 index 0000000000..d8527270af --- /dev/null +++ b/packages/app/public/favicon-v2.ico @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.ico \ No newline at end of file diff --git a/packages/app/public/favicon-v2.svg b/packages/app/public/favicon-v2.svg new file mode 120000 index 0000000000..2600394cea --- /dev/null +++ b/packages/app/public/favicon-v2.svg @@ -0,0 +1 @@ +../../ui/src/assets/favicon/favicon-v2.svg \ No newline at end of file diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index c870228607..2600bd8e24 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -27,7 +27,7 @@ export function DialogEditProject(props: { project: LocalProject }) { const [store, setStore] = createStore({ name: defaultName(), color: props.project.icon?.color || "pink", - iconUrl: props.project.icon?.url || "", + iconUrl: props.project.icon?.override || "", saving: false, }) @@ -79,7 +79,7 @@ export function DialogEditProject(props: { project: LocalProject }) { await globalSDK.client.project.update({ projectID: props.project.id, name, - icon: { color: store.color, url: store.iconUrl }, + icon: { color: store.color, override: store.iconUrl }, }) setStore("saving", false) dialog.close() diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 81222709f2..1c30cee98f 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -300,7 +300,8 @@ export const PromptInput: Component = (props) => { event.stopPropagation() const items = Array.from(clipboardData.items) - const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) + const fileItems = items.filter((item) => item.kind === "file") + const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type)) if (imageItems.length > 0) { for (const item of imageItems) { @@ -310,7 +311,16 @@ export const PromptInput: Component = (props) => { return } + if (fileItems.length > 0) { + showToast({ + title: "Unsupported paste", + description: "Only images or PDFs can be pasted here.", + }) + return + } + const plainText = clipboardData.getData("text/plain") ?? "" + if (!plainText) return addPart({ type: "text", content: plainText, start: 0, end: 0 }) } diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index a8da156092..d7d09aa399 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -33,8 +33,6 @@ type SessionTabs = { type SessionView = { scroll: Record reviewOpen?: string[] - terminalOpened?: boolean - reviewPanelOpened?: boolean } export type LocalProject = Partial & { worktree: string; expanded: boolean } @@ -78,9 +76,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, terminal: { height: 280, + opened: false, }, review: { diffStyle: "split" as ReviewDiffStyle, + panelOpened: true, }, session: { width: 600, @@ -172,7 +172,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const current = store.sessionView[sessionKey] const keep = meta.active ?? sessionKey if (!current) { - setStore("sessionView", sessionKey, { scroll: next, terminalOpened: false, reviewPanelOpened: true }) + setStore("sessionView", sessionKey, { scroll: next }) prune(keep) return } @@ -208,10 +208,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const usedColors = new Set() + const [colors, setColors] = createStore>({}) - function pickAvailableColor(): AvatarColorKey { - const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c)) + function pickAvailableColor(used: Set): AvatarColorKey { + const available = AVATAR_COLOR_KEYS.filter((c) => !used.has(c)) if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)] return available[Math.floor(Math.random() * available.length)] } @@ -222,24 +222,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const metadata = projectID ? globalSync.data.project.find((x) => x.id === projectID) : globalSync.data.project.find((x) => x.worktree === project.worktree) - return [ - { - ...(metadata ?? {}), - ...project, - icon: { url: metadata?.icon?.url, color: metadata?.icon?.color }, + return { + ...(metadata ?? {}), + ...project, + icon: { + url: metadata?.icon?.url, + override: metadata?.icon?.override, + color: metadata?.icon?.color, }, - ] - } - - function colorize(project: LocalProject) { - if (project.icon?.color) return project - const color = pickAvailableColor() - usedColors.add(color) - project.icon = { ...project.icon, color } - if (project.id) { - globalSdk.client.project.update({ projectID: project.id, icon: { color } }) } - return project } const roots = createMemo(() => { @@ -277,8 +268,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) }) - const enriched = createMemo(() => server.projects.list().flatMap(enrich)) - const list = createMemo(() => enriched().flatMap(colorize)) + const enriched = createMemo(() => server.projects.list().map(enrich)) + const list = createMemo(() => { + const projects = enriched() + return projects.map((project) => { + const color = project.icon?.color ?? colors[project.worktree] + if (!color) return project + const icon = project.icon ? { ...project.icon, color } : { color } + return { ...project, icon } + }) + }) + + createEffect(() => { + const projects = enriched() + if (projects.length === 0) return + + const used = new Set() + for (const project of projects) { + const color = project.icon?.color ?? colors[project.worktree] + if (color) used.add(color) + } + + for (const project of projects) { + if (project.icon?.color) continue + if (colors[project.worktree]) continue + const color = pickAvailableColor(used) + used.add(color) + setColors(project.worktree, color) + if (!project.id) continue + void globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + } + }) onMount(() => { Promise.all( @@ -379,31 +399,31 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( touch(sessionKey) scroll.seed(sessionKey) const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} }) - const terminalOpened = createMemo(() => s().terminalOpened ?? false) - const reviewPanelOpened = createMemo(() => s().reviewPanelOpened ?? true) + const terminalOpened = createMemo(() => store.terminal?.opened ?? false) + const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) function setTerminalOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.terminal if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: next, reviewPanelOpened: true }) + setStore("terminal", { height: 280, opened: next }) return } - const value = current.terminalOpened ?? false + const value = current.opened ?? false if (value === next) return - setStore("sessionView", sessionKey, "terminalOpened", next) + setStore("terminal", "opened", next) } function setReviewPanelOpened(next: boolean) { - const current = store.sessionView[sessionKey] + const current = store.review if (!current) { - setStore("sessionView", sessionKey, { scroll: {}, terminalOpened: false, reviewPanelOpened: next }) + setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) return } - const value = current.reviewPanelOpened ?? true + const value = current.panelOpened ?? true if (value === next) return - setStore("sessionView", sessionKey, "reviewPanelOpened", next) + setStore("review", "panelOpened", next) } return { @@ -444,8 +464,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( if (!current) { setStore("sessionView", sessionKey, { scroll: {}, - terminalOpened: false, - reviewPanelOpened: true, reviewOpen: open, }) return diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 28741098c8..8c4662926a 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -37,7 +37,7 @@ const platform: Platform = { .then(() => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96.png", + icon: "https://opencode.ai/favicon-96x96-v2.png", }) notification.onclick = () => { window.focus() diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 1f2f04fa3e..e7808501b0 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -543,7 +543,7 @@ export default function Layout(props: ParentProps) { running: number } - const prefetchChunk = 200 + const prefetchChunk = 600 const prefetchConcurrency = 1 const prefetchPendingLimit = 6 const prefetchToken = { value: 0 } @@ -1433,10 +1433,11 @@ export default function Layout(props: ParentProps) { getLabel={messageLabel} onMessageSelect={(message) => { if (!isActive()) { - navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`) + sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`) + navigate(`${props.slug}/session/${props.session.id}`) return } - window.location.hash = `message-${message.id}` + window.history.replaceState(null, "", `#message-${message.id}`) window.dispatchEvent(new HashChangeEvent("hashchange")) }} size="normal" @@ -1970,7 +1971,7 @@ export default function Layout(props: ParentProps) { transform: "translate3d(52px, 0, 0)", }} > - + {project()?.worktree.replace(homedir(), "~")} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index ca04c53760..29f79613d9 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -167,6 +167,7 @@ export default function Page() { const sdk = useSDK() const prompt = usePrompt() const permission = usePermission() + const [pendingMessage, setPendingMessage] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) const view = createMemo(() => layout.view(sessionKey())) @@ -530,7 +531,7 @@ export default function Page() { title: "Cycle thinking effort", description: "Switch to the next effort level", category: "Model", - keybind: "shift+mod+t", + keybind: "shift+mod+d", onSelect: () => { local.model.variant.cycle() }, @@ -943,17 +944,30 @@ export default function Page() { window.history.replaceState(null, "", `#${anchor(id)}`) } + createEffect(() => { + const sessionID = params.id + if (!sessionID) return + const raw = sessionStorage.getItem("opencode.pendingMessage") + if (!raw) return + const parts = raw.split("|") + const pendingSessionID = parts[0] + const messageID = parts[1] + if (!pendingSessionID || !messageID) return + if (pendingSessionID !== sessionID) return + + sessionStorage.removeItem("opencode.pendingMessage") + setPendingMessage(messageID) + }) + const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { const root = scroller - if (!root) { - el.scrollIntoView({ behavior, block: "start" }) - return - } + if (!root) return false const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() const top = a.top - b.top + root.scrollTop root.scrollTo({ top, behavior }) + return true } const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { @@ -967,7 +981,15 @@ export default function Page() { requestAnimationFrame(() => { const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + scrollToElement(next, behavior) + }) + return + } + scrollToElement(el, behavior) }) updateHash(message.id) @@ -975,10 +997,57 @@ export default function Page() { } const el = document.getElementById(anchor(message.id)) - if (el) scrollToElement(el, behavior) + if (!el) { + updateHash(message.id) + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) + return + } + if (scrollToElement(el, behavior)) { + updateHash(message.id) + return + } + + requestAnimationFrame(() => { + const next = document.getElementById(anchor(message.id)) + if (!next) return + if (!scrollToElement(next, behavior)) return + }) updateHash(message.id) } + const applyHash = (behavior: ScrollBehavior) => { + const hash = window.location.hash.slice(1) + if (!hash) { + autoScroll.forceScrollToBottom() + return + } + + const match = hash.match(/^message-(.+)$/) + if (match) { + const msg = visibleUserMessages().find((m) => m.id === match[1]) + if (msg) { + scrollToMessage(msg, behavior) + return + } + + // If we have a message hash but the message isn't loaded/rendered yet, + // don't fall back to "bottom". We'll retry once messages arrive. + return + } + + const target = document.getElementById(hash) + if (target) { + scrollToElement(target, behavior) + return + } + + autoScroll.forceScrollToBottom() + } + const getActiveMessageId = (container: HTMLDivElement) => { const cutoff = container.scrollTop + 100 const nodes = container.querySelectorAll("[data-message-id]") @@ -1019,31 +1088,47 @@ export default function Page() { if (!sessionID || !ready) return requestAnimationFrame(() => { - const hash = window.location.hash.slice(1) - if (!hash) { - autoScroll.forceScrollToBottom() - return - } - - const hashTarget = document.getElementById(hash) - if (hashTarget) { - scrollToElement(hashTarget, "auto") - return - } - - const match = hash.match(/^message-(.+)$/) - if (match) { - const msg = visibleUserMessages().find((m) => m.id === match[1]) - if (msg) { - scrollToMessage(msg, "auto") - return - } - } - - autoScroll.forceScrollToBottom() + applyHash("auto") }) }) + // Retry message navigation once the target message is actually loaded. + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + // dependencies + visibleUserMessages().length + store.turnStart + + const targetId = + pendingMessage() ?? + (() => { + const hash = window.location.hash.slice(1) + const match = hash.match(/^message-(.+)$/) + if (!match) return undefined + return match[1] + })() + if (!targetId) return + if (store.messageId === targetId) return + + const msg = visibleUserMessages().find((m) => m.id === targetId) + if (!msg) return + if (pendingMessage() === targetId) setPendingMessage(undefined) + requestAnimationFrame(() => scrollToMessage(msg, "auto")) + }) + + createEffect(() => { + const sessionID = params.id + const ready = messagesReady() + if (!sessionID || !ready) return + + const handler = () => requestAnimationFrame(() => applyHash("auto")) + window.addEventListener("hashchange", handler) + onCleanup(() => window.removeEventListener("hashchange", handler)) + }) + createEffect(() => { document.addEventListener("keydown", handleKeyDown) }) @@ -1163,9 +1248,9 @@ export default function Page() { -
- -
No changes in this session yet.
+
+ +
No changes in this session yet
@@ -1438,9 +1523,9 @@ export default function Page() { -
- -
No changes in this session yet.
+
+ +
No changes in this session yet
diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 2f44637fc8..83ff605cff 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index a9bb2706d4..eb86fb525b 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 6ada8abb05..6cfdaab757 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.25", + "version": "1.1.26", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/function/src/auth.ts b/packages/console/function/src/auth.ts index 082564b21c..ee68dffff5 100644 --- a/packages/console/function/src/auth.ts +++ b/packages/console/function/src/auth.ts @@ -35,7 +35,7 @@ export const subjects = createSubjects({ const MY_THEME: Theme = { ...THEME_OPENAUTH, - logo: "https://opencode.ai/favicon.svg", + logo: "https://opencode.ai/favicon-v2.svg", } export default { diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 7fe57cc79a..ab2fd76f8b 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.25", + "version": "1.1.26", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/index.html b/packages/desktop/index.html index d7d439ab8a..f03666d5e7 100644 --- a/packages/desktop/index.html +++ b/packages/desktop/index.html @@ -4,10 +4,10 @@ OpenCode - - - - + + + + diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 9114ff5621..0b95f75487 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 6cd77d7d55..a06270b13f 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -253,7 +253,7 @@ const createPlatform = (password: Accessor): Platform => ({ .then(() => { const notification = new Notification(title, { body: description ?? "", - icon: "https://opencode.ai/favicon-96x96.png", + icon: "https://opencode.ai/favicon-96x96-v2.png", }) notification.onclick = () => { const win = getCurrentWindow() diff --git a/packages/docs/docs.json b/packages/docs/docs.json index 4461f8253b..93dff10f8c 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -7,7 +7,7 @@ "light": "#07C983", "dark": "#15803D" }, - "favicon": "/favicon.svg", + "favicon": "/favicon-v2.svg", "navigation": { "tabs": [ { diff --git a/packages/docs/favicon-v2.svg b/packages/docs/favicon-v2.svg new file mode 100644 index 0000000000..b785c738bf --- /dev/null +++ b/packages/docs/favicon-v2.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index e80a58b2d7..1a40af1c79 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.25", + "version": "1.1.26", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index dccac3f933..2327c61e66 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.25" +version = "1.1.26" 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.25/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.25/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.25/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/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.25/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.26/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index ad83a519c2..e5b0f62b92 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.25", + "version": "1.1.26", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index e191819347..297abc8598 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.25", + "version": "1.1.26", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index e27c32dfb2..718929d445 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -85,6 +85,7 @@ export function Autocomplete(props: { index: 0, selected: 0, visible: false as AutocompleteRef["visible"], + input: "keyboard" as "keyboard" | "mouse", }) const [positionTick, setPositionTick] = createSignal(0) @@ -128,6 +129,14 @@ export function Autocomplete(props: { return props.input().getTextRange(store.index + 1, props.input().cursorOffset) }) + // When the filter changes due to how TUI works, the mousemove might still be triggered + // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so + // that the mouseover event doesn't trigger when filtering. + createEffect(() => { + filter() + setStore("input", "keyboard") + }) + function insertPart(text: string, part: PromptInfo["parts"][number]) { const input = props.input() const currentCursorOffset = input.cursorOffset @@ -525,11 +534,13 @@ export function Autocomplete(props: { const isNavDown = name === "down" || (ctrlOnly && name === "n") if (isNavUp) { + setStore("input", "keyboard") move(-1) e.preventDefault() return } if (isNavDown) { + setStore("input", "keyboard") move(1) e.preventDefault() return @@ -612,7 +623,17 @@ export function Autocomplete(props: { paddingRight={1} backgroundColor={index === store.selected ? theme.primary : undefined} flexDirection="row" - onMouseOver={() => moveTo(index)} + onMouseMove={() => { + setStore("input", "mouse") + }} + onMouseOver={() => { + if (store.input !== "mouse") return + moveTo(index) + }} + onMouseDown={() => { + setStore("input", "mouse") + moveTo(index) + }} onMouseUp={() => select()} > diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.tsx b/packages/opencode/src/cli/cmd/tui/component/tips.tsx index fe2e7ca216..3f0318e269 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -106,7 +106,7 @@ const TIPS = [ "Use plugins to send OS notifications when sessions complete", "Create a plugin to prevent OpenCode from reading sensitive files", "Use {highlight}opencode run{/highlight} for non-interactive scripting", - "Use {highlight}opencode run --continue{/highlight} to resume the last session", + "Use {highlight}opencode --continue{/highlight} to resume the last session", "Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI", "Use {highlight}--format json{/highlight} for machine-readable output in scripts", "Run {highlight}opencode serve{/highlight} for headless API access to OpenCode", diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0edc911344..392cfb7f12 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -241,9 +241,27 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ event.properties.info.sessionID, produce((draft) => { draft.splice(result.index, 0, event.properties.info) - if (draft.length > 100) draft.shift() }), ) + const updated = store.message[event.properties.info.sessionID] + if (updated.length > 100) { + const oldest = updated[0] + batch(() => { + setStore( + "message", + event.properties.info.sessionID, + produce((draft) => { + draft.shift() + }), + ) + setStore( + "part", + produce((draft) => { + delete draft[oldest.id] + }), + ) + }) + } break } case "message.removed": { diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 5c37a493df..618bf3b3cb 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -52,6 +52,7 @@ export function DialogSelect(props: DialogSelectProps) { const [store, setStore] = createStore({ selected: 0, filter: "", + input: "keyboard" as "keyboard" | "mouse", }) createEffect( @@ -83,6 +84,14 @@ export function DialogSelect(props: DialogSelectProps) { return result }) + // When the filter changes due to how TUI works, the mousemove might still be triggered + // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard + // that the mouseover event doesn't trigger when filtering. + createEffect(() => { + filtered() + setStore("input", "keyboard") + }) + const grouped = createMemo(() => { const result = pipe( filtered(), @@ -157,12 +166,15 @@ export function DialogSelect(props: DialogSelectProps) { const keybind = useKeybind() useKeyboard((evt) => { + setStore("input", "keyboard") + if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1) if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1) if (evt.name === "pageup") move(-10) if (evt.name === "pagedown") move(10) if (evt.name === "home") moveTo(0) if (evt.name === "end") moveTo(flat().length - 1) + if (evt.name === "return") { const option = selected() if (option) { @@ -259,11 +271,20 @@ export function DialogSelect(props: DialogSelectProps) { { + setStore("input", "mouse") + }} onMouseUp={() => { option.onSelect?.(dialog) props.onSelect?.(option) }} onMouseOver={() => { + if (store.input !== "mouse") return + const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) + if (index === -1) return + moveTo(index) + }} + onMouseDown={() => { const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) if (index === -1) return moveTo(index) @@ -337,6 +358,7 @@ function Option(props: { fg={props.active ? fg : props.current ? theme.primary : theme.text} attributes={props.active ? TextAttributes.BOLD : undefined} overflow="hidden" + wrapMode="none" paddingLeft={3} > {Locale.truncate(props.title, 61)} diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 834cbee1ed..0d18173565 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -162,34 +162,32 @@ export namespace Ripgrep { }) } if (config.extension === "zip") { - if (config.extension === "zip") { - const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()]))) - const entries = await zipFileReader.getEntries() - let rgEntry: any - for (const entry of entries) { - if (entry.filename.endsWith("rg.exe")) { - rgEntry = entry - break - } + const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()]))) + const entries = await zipFileReader.getEntries() + let rgEntry: any + for (const entry of entries) { + if (entry.filename.endsWith("rg.exe")) { + rgEntry = entry + break } - - if (!rgEntry) { - throw new ExtractionFailedError({ - filepath: archivePath, - stderr: "rg.exe not found in zip archive", - }) - } - - const rgBlob = await rgEntry.getData(new BlobWriter()) - if (!rgBlob) { - throw new ExtractionFailedError({ - filepath: archivePath, - stderr: "Failed to extract rg.exe from zip archive", - }) - } - await Bun.write(filepath, await rgBlob.arrayBuffer()) - await zipFileReader.close() } + + if (!rgEntry) { + throw new ExtractionFailedError({ + filepath: archivePath, + stderr: "rg.exe not found in zip archive", + }) + } + + const rgBlob = await rgEntry.getData(new BlobWriter()) + if (!rgBlob) { + throw new ExtractionFailedError({ + filepath: archivePath, + stderr: "Failed to extract rg.exe from zip archive", + }) + } + await Bun.write(filepath, await rgBlob.arrayBuffer()) + await zipFileReader.close() } await fs.unlink(archivePath) if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 2cec78623d..40ebb21ea5 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -25,6 +25,7 @@ export namespace Project { icon: z .object({ url: z.string().optional(), + override: z.string().optional(), color: z.string().optional(), }) .optional(), @@ -190,6 +191,7 @@ export namespace Project { if (!existing.sandboxes) existing.sandboxes = [] if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing) + const result: Info = { ...existing, worktree, @@ -213,6 +215,7 @@ export namespace Project { export async function discover(input: Info) { if (input.vcs !== "git") return + if (input.icon?.override) return if (input.icon?.url) return const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}") const matches = await Array.fromAsync( @@ -293,6 +296,7 @@ export namespace Project { ...draft.icon, } if (input.icon.url !== undefined) draft.icon.url = input.icon.url + if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined if (input.icon.color !== undefined) draft.icon.color = input.icon.color } draft.time.updated = Date.now() diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts index 8bffbd54a2..ba34eb48f5 100644 --- a/packages/opencode/src/tool/batch.ts +++ b/packages/opencode/src/tool/batch.ts @@ -33,8 +33,8 @@ export const BatchTool = Tool.define("batch", async () => { const { Session } = await import("../session") const { Identifier } = await import("../id/id") - const toolCalls = params.tool_calls.slice(0, 10) - const discardedCalls = params.tool_calls.slice(10) + const toolCalls = params.tool_calls.slice(0, 25) + const discardedCalls = params.tool_calls.slice(25) const { ToolRegistry } = await import("./registry") const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" }) @@ -139,14 +139,14 @@ export const BatchTool = Tool.define("batch", async () => { state: { status: "error", input: call.parameters, - error: "Maximum of 10 tools allowed in batch", + error: "Maximum of 25 tools allowed in batch", time: { start: now, end: now }, }, }) results.push({ success: false as const, tool: call.tool, - error: new Error("Maximum of 10 tools allowed in batch"), + error: new Error("Maximum of 25 tools allowed in batch"), }) } diff --git a/packages/opencode/src/tool/batch.txt b/packages/opencode/src/tool/batch.txt index b1b6a6010f..565eb4dd43 100644 --- a/packages/opencode/src/tool/batch.txt +++ b/packages/opencode/src/tool/batch.txt @@ -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: -- 1–10 tool calls per batch +- 1–20 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. diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 2734901808..ee7cf23b9f 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f3b12aa8c9..f69224d832 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 59b7f06963..706d0f9c22 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -302,6 +302,7 @@ export class Project extends HeyApiClient { name?: string icon?: { url?: string + override?: string color?: string } }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 75540f9072..b7e72fbad8 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -25,6 +25,7 @@ export type Project = { name?: string icon?: { url?: string + override?: string color?: string } time: { @@ -2229,6 +2230,7 @@ export type ProjectUpdateData = { name?: string icon?: { url?: string + override?: string color?: string } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 08dd98fd9b..c1be820f26 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -231,6 +231,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } @@ -5796,6 +5799,9 @@ "url": { "type": "string" }, + "override": { + "type": "string" + }, "color": { "type": "string" } diff --git a/packages/slack/package.json b/packages/slack/package.json index d544b89e38..37b116ec2b 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 0b490591c3..7079384a54 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.25", + "version": "1.1.26", "type": "module", "license": "MIT", "exports": { diff --git a/packages/ui/src/assets/favicon/apple-touch-icon-v2.png b/packages/ui/src/assets/favicon/apple-touch-icon-v2.png new file mode 100644 index 0000000000..70fd01b0ea Binary files /dev/null and b/packages/ui/src/assets/favicon/apple-touch-icon-v2.png differ diff --git a/packages/ui/src/assets/favicon/favicon-96x96-v2.png b/packages/ui/src/assets/favicon/favicon-96x96-v2.png new file mode 100644 index 0000000000..15266d28f1 Binary files /dev/null and b/packages/ui/src/assets/favicon/favicon-96x96-v2.png differ diff --git a/packages/ui/src/assets/favicon/favicon-v2.ico b/packages/ui/src/assets/favicon/favicon-v2.ico new file mode 100644 index 0000000000..34ca0b9c01 Binary files /dev/null and b/packages/ui/src/assets/favicon/favicon-v2.ico differ diff --git a/packages/ui/src/assets/favicon/favicon-v2.svg b/packages/ui/src/assets/favicon/favicon-v2.svg new file mode 100644 index 0000000000..157edc4d75 --- /dev/null +++ b/packages/ui/src/assets/favicon/favicon-v2.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/ui/src/components/favicon.tsx b/packages/ui/src/components/favicon.tsx index 3462384d45..abb0e1f78c 100644 --- a/packages/ui/src/components/favicon.tsx +++ b/packages/ui/src/components/favicon.tsx @@ -3,9 +3,9 @@ import { Link, Meta } from "@solidjs/meta" export const Favicon = () => { return ( <> - - - + + + diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 9bf395e2ae..88bda32809 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -59,6 +59,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) const searchProps = () => (typeof props.search === "object" ? props.search : {}) + const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0 + createEffect(() => { if (props.filter !== undefined) { onInput(props.filter) @@ -234,7 +236,8 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void }) data-selected={item === props.current} onClick={() => handleSelect(item, i())} type="button" - onMouseMove={() => { + onMouseMove={(event) => { + if (!moved(event)) return setStore("mouseActive", true) setActive(props.key(item)) }} diff --git a/packages/ui/src/components/resize-handle.css b/packages/ui/src/components/resize-handle.css index 088bf92157..e4c8d474e5 100644 --- a/packages/ui/src/components/resize-handle.css +++ b/packages/ui/src/components/resize-handle.css @@ -5,10 +5,8 @@ &::after { content: ""; position: absolute; - background-color: var(--color-border-strong-base); opacity: 0; transition: opacity 0.15s ease-in-out; - border-radius: 2px; } &:hover::after, diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 5f8c0a16f6..034d302470 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -75,6 +75,17 @@ background-color: var(--background-stronger); z-index: -1; } + + &::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 32px; + background: linear-gradient(to bottom, var(--background-stronger), transparent); + pointer-events: none; + } } [data-slot="session-turn-response-trigger"] { diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index a918f0ae4f..360589f411 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -6,7 +6,6 @@ import { type PermissionRequest, TextPart, ToolPart, - UserMessage, } from "@opencode-ai/sdk/v2/client" import { useData } from "../context" import { useDiffComponent } from "../context/diff" @@ -21,8 +20,6 @@ import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { FileIcon } from "./file-icon" import { Icon } from "./icon" -import { IconButton } from "./icon-button" -import { Tooltip } from "./tooltip" import { Card } from "./card" import { Dynamic } from "solid-js/web" import { Button } from "./button" @@ -352,7 +349,6 @@ export function SessionTurn( const hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0) const hideResponsePart = createMemo(() => !working() && !!responsePartId()) - const [responseCopied, setResponseCopied] = createSignal(false) const [rootRef, setRootRef] = createSignal() const [stickyRef, setStickyRef] = createSignal() @@ -362,13 +358,6 @@ export function SessionTurn( const next = Math.ceil(height) root.style.setProperty("--session-turn-sticky-height", `${next}px`) } - const handleCopyResponse = async () => { - const content = response() - if (!content) return - await navigator.clipboard.writeText(content) - setResponseCopied(true) - setTimeout(() => setResponseCopied(false), 2000) - } function duration() { const msg = message() @@ -589,15 +578,6 @@ export function SessionTurn( {/* Response */}
-
- - - -

Response