Compare commits

...
5 Commits
56 changed files with 532 additions and 147 deletions
+35 -35
View File
@@ -32,7 +32,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -54,7 +54,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -112,7 +112,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.15",
"version": "2.0.16",
"bin": {
"opencode": "./bin/opencode.cjs",
"opencode2": "./bin/opencode2.cjs",
@@ -177,7 +177,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -203,7 +203,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -216,7 +216,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -252,7 +252,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -279,7 +279,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
@@ -296,7 +296,7 @@
},
"packages/console/mail": {
"name": "@opencode/console-mail",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -320,7 +320,7 @@
},
"packages/console/support": {
"name": "@opencode/console-support",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -340,7 +340,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/gateway": "3.0.104",
@@ -408,7 +408,7 @@
},
"packages/desktop": {
"name": "@opencode/desktop",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "5.0.0",
@@ -457,7 +457,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -494,7 +494,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -510,7 +510,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -529,7 +529,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -542,7 +542,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -556,7 +556,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -571,7 +571,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -610,7 +610,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -640,7 +640,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -655,7 +655,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -679,7 +679,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -700,7 +700,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -722,7 +722,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -757,7 +757,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -777,7 +777,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -811,7 +811,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -830,7 +830,7 @@
},
"packages/stats/server": {
"name": "@opencode/stats-server",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -876,7 +876,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -890,7 +890,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -925,7 +925,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -960,7 +960,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -997,7 +997,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1038,7 +1038,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.15",
"version": "2.0.16",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
@@ -214,6 +214,23 @@ for (const draft of ["empty-draft", "multiline-draft", "mixed-attachments"]) {
})
}
story.describe("touch attachments", () => {
story.use({ hasTouch: true, isMobile: true, viewport: { width: 390, height: 844 } })
story("keeps attachment remove buttons visible without hover", async ({ mount, page }) => {
const component = await mount("opencode-composer-flow--mixed-attachments")
expect(await page.evaluate(() => matchMedia("(hover: none)").matches)).toBe(true)
const remove = component.getByRole("button", { name: "Remove attachment", exact: true })
await expect(remove).not.toHaveCount(0)
for (const button of await remove.all()) await expect(button).toHaveCSS("opacity", "1")
const count = await remove.count()
await remove.first().tap()
await expect(remove).toHaveCount(count - 1)
})
})
story("renders a draft once and supports editing, caret restoration, and failure recovery", async ({ mount, page }) => {
await page.addInitScript(() => {
const replace = Element.prototype.replaceChildren
@@ -8,8 +8,13 @@ const directory = "C:/OpenCode/SubagentNavigation"
const projectID = "proj_subagent_navigation"
const parentID = "ses_subagent_parent"
const childID = "ses_subagent_child"
const grandchildID = "ses_subagent_grandchild"
const greatGrandchildID = "ses_subagent_great_grandchild"
const parentTitle = "Parent session"
const childTitle = "Subagent child session"
const grandchildTitle = "Nested subagent session"
const greatGrandchildTitle =
"Deep research subagent session investigating a very long chain of agent registry failures and navigation breadcrumbs"
// Child session pages derive their heading from the task part that spawned them.
const taskDescription = "Inspect child navigation"
@@ -25,6 +30,60 @@ test("navigates to a subagent child session missing from the session list", asyn
await expect(page.getByRole("button", { name: "Toggle review", exact: true })).toBeVisible()
})
test("opens a directly linked nested subagent in its root session tab", async ({ page }) => {
await setup(page, undefined, 1)
await page.goto(sessionHref(grandchildID), { waitUntil: "domcontentloaded" })
await expect(page.locator('[data-slot="session-title-parent"]')).toHaveText(childTitle)
await expect(page.locator('[data-slot="session-title-child"]')).toHaveText(grandchildTitle)
const tabs = page.locator('[data-slot="titlebar-tabs"] [data-titlebar-tab-slot]')
await expect(tabs.locator(`a[href="${sessionHref(grandchildID)}"]`)).toHaveCount(1)
await expect(tabs).toHaveCount(1)
await expect(tabs.locator('[data-slot="tab-title"]')).toHaveText(parentTitle)
})
test("shows the full ancestor path and navigates to an earlier subagent", async ({ page }) => {
await setup(page, undefined, 2)
await page.goto(sessionHref(greatGrandchildID), { waitUntil: "domcontentloaded" })
await expect(page.locator("[data-timeline-virtual-content]")).toBeAttached()
const header = page.locator("[data-session-title]")
await expect(header.locator('[data-slot="session-title-ancestor"]')).toHaveText([parentTitle, childTitle])
await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(grandchildTitle)
await expect(header.getByRole("heading", { name: greatGrandchildTitle })).toBeVisible()
await header.getByRole("button", { name: childTitle, exact: true }).click()
await expect(page).toHaveURL(sessionHref(childID))
await expect(page.locator("[data-session-title]").getByRole("heading", { name: childTitle })).toBeVisible()
await expect(page.locator('[data-slot="titlebar-tabs"] [data-titlebar-tab-slot]')).toHaveCount(1)
})
test("keeps the active nested session visible in a narrow desktop header", async ({ page }) => {
await page.setViewportSize({ width: 820, height: 720 })
await setup(page, undefined, 2)
await page.goto(sessionHref(greatGrandchildID), { waitUntil: "domcontentloaded" })
const header = page.locator("[data-session-title]")
const heading = header.getByRole("heading", { name: greatGrandchildTitle })
const separator = header.locator('[data-slot="session-title-ancestors"] + [data-slot="session-title-separator"]')
await expect(header.locator('[data-slot="session-title-ancestor"]')).toHaveText([parentTitle, childTitle])
await expect(heading).toBeInViewport()
await expect(separator).toBeInViewport()
const bounds = await heading.boundingBox()
expect(bounds).not.toBeNull()
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(820)
await page.evaluate(() => document.documentElement.setAttribute("dir", "rtl"))
await expect(heading).toBeInViewport()
await expect(separator).toBeInViewport()
const root = header.locator(`[data-slot="session-title-ancestor"][data-session-id="${parentID}"]`)
await root.focus()
await expect(root).toBeFocused()
await root.press("Enter")
await expect(page).toHaveURL(sessionHref(parentID))
})
test("returns to the parent session with Escape", async ({ page }) => {
await setup(page)
await openChildFromParent(page)
@@ -144,7 +203,7 @@ test("shows the not found fallback when the viewed session is deleted", async ({
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
})
async function setup(page: Page, events?: () => OpenCodeEvent[]) {
async function setup(page: Page, events?: () => OpenCodeEvent[], nestedDepth: 0 | 1 | 2 = 0) {
await mockOpenCodeServer(page, {
directory,
project: {
@@ -168,7 +227,14 @@ async function setup(page: Page, events?: () => OpenCodeEvent[]) {
connected: ["opencode"],
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
},
sessions: [session(parentID, parentTitle, 1700000000000), childSession()],
sessions: [
session(parentID, parentTitle, 1700000000000),
childSession(),
...(nestedDepth >= 1 ? [session(grandchildID, grandchildTitle, 1700000002000, { parentID: childID })] : []),
...(nestedDepth >= 2
? [session(greatGrandchildID, greatGrandchildTitle, 1700000003000, { parentID: grandchildID })]
: []),
],
pageMessages: (sessionID) => ({ items: sessionID === parentID ? parentMessages() : [] }),
events,
eventRetry: events ? 16 : undefined,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.15",
"version": "2.0.16",
"description": "",
"type": "module",
"exports": {
+3 -3
View File
@@ -542,7 +542,7 @@ export function ComposerAttachments(props: {
<button
type="button"
onClick={() => props.onCommentRemove?.(comment)}
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center hover-reveal group-hover:opacity-100"
aria-label={props.removeLabel}
>
<Icon name="outline-xmark" class="text-v2-icon-icon-contrast" />
@@ -589,7 +589,7 @@ export function ComposerAttachments(props: {
<button
type="button"
onClick={() => props.onAttachmentRemove(attachment)}
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center hover-reveal group-hover:opacity-100"
aria-label={props.removeLabel}
>
<Icon name="outline-xmark" class="text-v2-icon-icon-contrast" />
@@ -611,7 +611,7 @@ export function ComposerAttachments(props: {
<button
type="button"
onClick={() => props.onUploadCancel?.(upload)}
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center hover-reveal group-hover:opacity-100"
aria-label={i18n.t("ui.promptInput.cancelUpload")}
>
<Icon name="outline-xmark" class="text-v2-icon-icon-contrast" />
@@ -6,7 +6,7 @@ import { ProjectAvatar } from "@opencode/ui/project-avatar"
import { Tooltip } from "@opencode/ui/tooltip"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useNavigate } from "@solidjs/router"
import { createMemo, Show, type ParentProps } from "solid-js"
import { createMemo, For, Show, type ParentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { useServer } from "@/runtime/server/current"
import { ServerConnection } from "@/runtime/server/registry"
@@ -191,12 +191,96 @@ export function SessionProjectMenu(props: {
)
}
export function SessionAncestorTrail(props: {
sessionID: string
parentID: string
parentTitle?: string
trailing: boolean
}) {
const server = useServer()
const tabs = useTabs()
const navigate = useNavigate()
const language = useLanguage()
const ancestors = createMemo(() => {
const path: { id: string; title: string; direct: boolean }[] = []
const seen = new Set([props.sessionID])
let id: string | undefined = props.parentID
while (id && !seen.has(id)) {
seen.add(id)
const info = server.ctx.data.session.get(id)
path.unshift({
id,
title:
sessionTitle(info?.title ?? (id === props.parentID ? props.parentTitle : undefined)) ??
language.t("session.tab.session"),
direct: id === props.parentID,
})
id = info?.parentID
}
return path
})
const open = (id: string) => {
const tab = tabs.store.find(
(item) =>
item.type === "session" &&
item.server === server.key &&
(item.sessionId === props.sessionID || item.routeSessionId === props.sessionID),
)
if (tab?.type === "session") tabs.rememberSessionRoute(tab, id, server.ctx.data.session.get(id)?.parentID)
navigate(sessionHref(server.key, id))
}
return (
<div class="flex min-w-0 max-w-full items-center">
<div
data-slot="session-title-ancestors"
class="flex min-w-0 items-center overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<For each={ancestors()}>
{(ancestor, index) => (
<>
<button
type="button"
data-slot={ancestor.direct ? "session-title-parent" : "session-title-ancestor"}
data-session-id={ancestor.id}
title={ancestor.title}
dir="auto"
class="max-w-[min(200px,40vw)] shrink-0 truncate pl-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
onClick={() => open(ancestor.id)}
>
{ancestor.title}
</button>
<Show when={index() < ancestors().length - 1}>
<span
data-slot="session-title-separator"
class="-translate-y-[0.5px] shrink-0 pl-2 pr-1 text-[11px] font-medium text-v2-text-text-faint"
aria-hidden="true"
>
/
</span>
</Show>
</>
)}
</For>
</div>
<Show when={props.trailing}>
<span
data-slot="session-title-separator"
class="-translate-y-[0.5px] shrink-0 pl-2 pr-1 text-[11px] font-medium text-v2-text-text-faint"
aria-hidden="true"
>
/
</span>
</Show>
</div>
)
}
export function SessionIdentityHeader(props: { sessionID: string; session?: SessionInfo }) {
const server = useServer()
const tabs = useTabs()
const language = useLanguage()
const pending = createMemo(() => tabs.pendingSession(server.key, props.sessionID))
const navigate = useNavigate()
const tab = createMemo(() =>
tabs.store.find(
(item) =>
@@ -250,53 +334,30 @@ export function SessionIdentityHeader(props: { sessionID: string; session?: Sess
)
})
const workspaceSession = createMemo(() => !!pending() || isWorkspaceDirectory(project(), directory() ?? ""))
const navigateParent = () => {
const id = parentID()
const current = tab()
if (!id || current?.type !== "session") return
tabs.rememberSessionRoute(current, id, parent()?.parentID)
navigate(sessionHref(server.key, id))
}
return (
<Show when={title() || parentTitle()}>
<SessionTitleHeader>
<div class="flex h-12 w-full items-center justify-between gap-2">
<div class="flex min-w-0 flex-1 items-center gap-1">
<div class="flex min-w-0 w-full flex-1 items-center gap-0.5">
<SessionProjectMenu
project={project()}
directory={directory()}
workspace={workspaceSession()}
/>
<Show when={parentTitle()}>
{(value) => (
<button
type="button"
data-slot="session-title-parent"
dir="auto"
class="min-w-0 max-w-[40%] truncate pl-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
onClick={navigateParent}
>
{value()}
</button>
<SessionProjectMenu project={project()} directory={directory()} workspace={workspaceSession()} />
<Show when={parentID()}>
{(id) => (
<SessionAncestorTrail
sessionID={props.sessionID}
parentID={id()}
parentTitle={parentTitle()}
trailing={!!title()}
/>
)}
</Show>
<Show when={parentTitle() && title()}>
<span
data-slot="session-title-separator"
class="-translate-y-[0.5px] pl-2 pr-1 text-[11px] font-medium text-v2-text-text-faint"
aria-hidden="true"
>
/
</span>
</Show>
<Show when={title()}>
{(value) => (
<h1
data-slot={parentID() ? "session-title-child" : undefined}
dir="auto"
class="w-fit truncate rounded-[6px] px-1 py-1 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base"
classList={{ "max-w-[45%] shrink-0": !!parentID() }}
>
{value()}
</h1>
@@ -34,7 +34,7 @@ import { containsDirectory, isWorkspaceDirectory } from "@/workspaces/paths"
import { projectForSession } from "@/shell/layout/helpers"
import { parseCommentNote, readPromptPresentation } from "@/composer/comment-note"
import { useCommand } from "@/shell/commands/command"
import { SessionProjectMenu, SessionTitleHeader } from "../session-identity-header"
import { SessionAncestorTrail, SessionProjectMenu, SessionTitleHeader } from "../session-identity-header"
import { SessionHeaderSpacer } from "@/session/header/session-header"
import type { BackgroundTask } from "../summary/background"
@@ -413,21 +413,14 @@ function MessageTimelineView(
workspace={workspaceSession()}
/>
<Show when={parentID()}>
<button
type="button"
data-slot="session-title-parent"
class="min-w-0 max-w-[40%] truncate pl-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
onClick={props.action.navigateParent}
>
{parentTitle()}
</button>
<span
data-slot="session-title-separator"
class="-translate-y-[0.5px] pl-2 pr-1 text-[11px] font-medium text-v2-text-text-faint"
aria-hidden="true"
>
/
</span>
{(id) => (
<SessionAncestorTrail
sessionID={sessionID() ?? ""}
parentID={id()}
parentTitle={parentTitle()}
trailing={!!(childTitle() || title.editing)}
/>
)}
</Show>
<Show when={childTitle() || title.editing}>
<Show
@@ -436,6 +429,7 @@ function MessageTimelineView(
<h1
data-slot="session-title-child"
class="truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base w-fit rounded-[6px] px-1 py-1 hover:bg-v2-overlay-simple-overlay-hover"
classList={{ "max-w-[45%] shrink-0": !!parentID() }}
onClick={openTitleEditor}
>
{childTitle()}
@@ -451,6 +445,7 @@ function MessageTimelineView(
value={title.draft}
disabled={props.pending.rename()}
class="block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base field-sizing-content rounded-[6px] px-1 py-1"
classList={{ "max-w-[45%] shrink-0": !!parentID() }}
style={{
"--inline-input-shadow": "none",
"text-align": "start",
@@ -394,7 +394,7 @@ export const SettingsGeneral: Component = () => {
</div>
</SettingsRow>
<Show when={mobile() && import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"}>
<Show when={mobile()}>
<SettingsRow
title={language.t("settings.general.row.mobileTitlebarBottom.title")}
description={language.t("settings.general.row.mobileTitlebarBottom.description")}
+2 -2
View File
@@ -12,7 +12,7 @@ type Entry<Tab> = {
description?: Label
section?: Label
subtab?: "mcps" | "plugins" | "skills" | "lsps"
available?: "desktop" | "browser" | "dev" | "mobile-dev"
available?: "desktop" | "browser" | "mobile"
}
export const clientSettings: Entry<SettingsRootTab>[] = [
@@ -83,7 +83,7 @@ export const clientSettings: Entry<SettingsRootTab>[] = [
tab: "general",
label: "settings.general.row.mobileTitlebarBottom.title",
target: "settings-mobile-titlebar-bottom",
available: "mobile-dev",
available: "mobile",
},
{
tab: "general",
+1 -3
View File
@@ -17,7 +17,6 @@ export function settingsSearchIndex(input: {
servers: readonly SettingsSearchServer[]
desktop: boolean
browser: boolean
dev: boolean
mobile: boolean
translate: ReturnType<typeof useLanguage>["t"]
}) {
@@ -57,8 +56,7 @@ export function settingsSearchIndex(input: {
clientSettings.forEach((entry) => {
if (entry.available === "desktop" && !input.desktop) return
if (entry.available === "browser" && !input.browser) return
if ((entry.available === "dev" || entry.available === "mobile-dev") && !input.dev) return
if (entry.available === "mobile-dev" && !input.mobile) return
if (entry.available === "mobile" && !input.mobile) return
add(entry, { type: "root", tab: entry.tab, target: entry.target }, "")
})
input.servers.forEach((server) => {
@@ -18,7 +18,6 @@ function index(input: Partial<Parameters<typeof settingsSearchIndex>[0]> = {}) {
servers,
desktop: false,
browser: false,
dev: false,
mobile: false,
translate: (key) => strings[key],
...input,
@@ -49,7 +48,7 @@ describe("settings search index", () => {
expect(rankSettings("font", items, root)).toHaveLength(3)
})
test("only advertises settings supported by this platform and channel", () => {
test("only advertises settings supported by this platform", () => {
const targets = (input: Parameters<typeof index>[0]) => index(input).map((item) => item.view.target)
expect(targets({})).not.toContain("settings-pinch-zoom")
expect(targets({})).not.toContain("settings-experimental-browser")
@@ -57,9 +56,9 @@ describe("settings search index", () => {
expect(targets({})).toContain("settings-tab-layout")
expect(targets({ desktop: true })).toContain("settings-pinch-zoom")
expect(targets({ browser: true })).not.toContain("settings-experimental-browser")
expect(targets({ dev: true })).not.toContain("settings-show-project-icon")
expect(targets({ dev: true })).not.toContain("settings-mobile-titlebar-bottom")
expect(targets({ dev: true, mobile: true })).toContain("settings-mobile-titlebar-bottom")
expect(targets({})).not.toContain("settings-show-project-icon")
expect(targets({})).not.toContain("settings-mobile-titlebar-bottom")
expect(targets({ mobile: true })).toContain("settings-mobile-titlebar-bottom")
})
test("uses section labels and stable identities independent of translated text", () => {
-1
View File
@@ -75,7 +75,6 @@ export function SettingsSearch() {
servers: inventory(),
desktop: platform.platform === "desktop",
browser: !!platform.browserPane,
dev: import.meta.env.VITE_OPENCODE_CHANNEL !== "prod",
mobile: mobile(),
translate: language.t,
}),
+26 -6
View File
@@ -33,6 +33,7 @@ import { projectForSession } from "@/shell/layout/helpers"
import { useSettingsDialog } from "@/settings/command"
import { updaterAction } from "@/shell/updates/action"
import type { UpdaterState } from "@/shell/updates/types"
import { rootSession } from "@/shell/routes/session"
import devIcon from "../../../../desktop/icons/dev/64x64.png"
import betaIcon from "../../../../desktop/icons/beta/64x64.png"
@@ -185,7 +186,7 @@ export function Titlebar(props: {
const route = layout.route()
return route.type === "session" && !!tabs.pendingSession(route.server, route.sessionId)
})
const [loadedSession] = createResource(
const [resolvedSession] = createResource(
() => {
const route = layout.route()
if (route.type !== "session") return undefined
@@ -193,7 +194,23 @@ export function Titlebar(props: {
const conn = global.servers.list().find((item) => ServerConnection.key(item) === route.server)
return conn ? { route, ctx: global.ensureServerCtx(conn) } : undefined
},
({ route, ctx }) => ctx.sdk.api.session.get({ sessionID: route.sessionId }).catch(() => {}),
async ({ route, ctx }) => {
const info = await ctx.sdk.api.session
.get({ sessionID: route.sessionId })
.catch(() => ctx.data.session.get(route.sessionId))
if (!info) return
ctx.data.session.remember(info)
const rootID = await rootSession(info, async (id) => {
const cached = ctx.data.session.get(id)
if (cached) return cached
const ancestor = await ctx.sdk.api.session.get({ sessionID: id })
ctx.data.session.remember(ancestor)
return ancestor
})
.then((root) => root.id)
.catch(() => ctx.data.session.root(info.id))
return { info, rootID }
},
)
const session = createMemo(() => {
const route = layout.route()
@@ -202,8 +219,8 @@ export function Titlebar(props: {
const conn = global.servers.list().find((item) => ServerConnection.key(item) === route.server)
const cached = conn ? global.ensureServerCtx(conn).data.session.get(route.sessionId) : undefined
if (cached) return cached
const loaded = loadedSession()
return loaded?.id === route.sessionId ? loaded : undefined
const resolved = resolvedSession()
return resolved?.info.id === route.sessionId ? resolved.info : undefined
})
const matchRoute = (route: LayoutRoute) => {
@@ -221,7 +238,8 @@ export function Titlebar(props: {
if (main) return main
const s = session()
if (s?.parentID) {
const parentID = s.parentID
const resolved = resolvedSession()
const parentID = resolved?.info.id === s.id ? resolved.rootID : s.parentID
const parent = tabsStore.find(
(item) => item.type === "session" && item.server === route.server && item.sessionId === parentID,
)
@@ -256,7 +274,9 @@ export function Titlebar(props: {
}
const s = session()
if (!s) return
const sessionId = s.parentID ?? s.id
const resolved = resolvedSession()
if (s.parentID && resolved?.info.id !== s.id) return
const sessionId = resolved?.info.id === s.id ? resolved.rootID : s.id
const next = { server: route.server, sessionId }
tabsStoreActions.addSessionTab(next)
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/client",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/codemode",
"version": "2.0.15",
"version": "2.0.16",
"description": "Effect-native confined code execution over schema-described tools",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-app",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/console-core",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-function",
"version": "2.0.15",
"version": "2.0.16",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-mail",
"version": "2.0.15",
"version": "2.0.16",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-support",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.15",
"version": "2.0.16",
"name": "@opencode/core",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/desktop",
"private": true,
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/enterprise",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "2.0.15",
"version": "2.0.16",
"$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": "2.0.15",
"version": "2.0.16",
"name": "@opencode/http-recorder",
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/httpapi-codegen",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/latex",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/merman",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin-browser",
"version": "2.0.15",
"version": "2.0.16",
"description": "OpenCode's desktop browser plugin",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/protocol",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/schema",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.15",
"version": "2.0.16",
"name": "@opencode/sdk",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/server",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
@@ -163,6 +163,89 @@ story("mounts cached completed Markdown with sanitized HTML and decorations", as
await expect(markdown).toHaveAttribute("data-markdown-ready", "")
})
story("shows a stable GitHub mark without changing link text or other sites", async ({ page }) => {
await page.evaluate(async (fixture) => {
const { mountMarkdown } = await import(fixture)
await mountMarkdown({
text: [
"[#540](https://github.com/anomalyco/opencode/pull/540)",
"[GitHub](https://github.com)",
"[other site](https://example.com/docs)",
"[lookalike](https://github.com.evil.example/pull/540)",
].join(" · "),
cached: true,
})
}, fixture)
const markdown = page.getByTestId("markdown-fixture").locator('[data-component="markdown"]')
await expect(markdown).toHaveAttribute("data-markdown-ready", "")
const github = markdown.getByRole("link", { name: "#540" })
await expect(github).toHaveAttribute("href", "https://github.com/anomalyco/opencode/pull/540")
await expect(github).toHaveText("#540")
expect(await github.evaluate((link) => getComputedStyle(link, "::before").width)).toBe("14px")
expect(await github.evaluate((link) => getComputedStyle(link, "::before").maskImage)).toContain("data:image/svg+xml")
expect(
await markdown.getByRole("link", { name: "GitHub" }).evaluate((link) => getComputedStyle(link, "::before").content),
).toBe('""')
for (const name of ["other site", "lookalike"]) {
expect(
await markdown.getByRole("link", { name }).evaluate((link) => getComputedStyle(link, "::before").content),
).toBe("none")
}
})
story("keeps favicon space stable across loading and failure without fetching private hosts", async ({ page }) => {
const requested: string[] = []
let release: () => void = () => undefined
const loading = new Promise<void>((resolve) => (release = resolve))
await page.route("https://www.google.com/s2/favicons?**", async (route) => {
requested.push(route.request().url())
if (route.request().url().includes("developer.mozilla.org")) await loading
if (route.request().url().includes("broken.example.org")) return route.abort()
await route.fulfill({ status: 200, contentType: "image/png", body: png })
})
await page.evaluate(async (fixture) => {
const { mountMarkdown } = await import(fixture)
await mountMarkdown({
text: [
"[docs](https://developer.mozilla.org/docs)",
"[missing](https://broken.example.org/docs)",
"[private](http://localhost:8080/docs)",
"[GitHub](https://github.com/anomalyco/opencode)",
].join(" · "),
cached: true,
})
}, fixture)
const markdown = page.getByTestId("markdown-fixture").locator('[data-component="markdown"]')
await expect(markdown).toHaveAttribute("data-markdown-ready", "")
const docs = markdown.getByRole("link", { name: "docs" })
const image = docs.locator(".markdown-link-favicon img")
await expect.poll(() => requested.some((url) => url.includes("developer.mozilla.org"))).toBe(true)
await expect(image).toHaveCSS("opacity", "0")
await expect(docs.locator(".markdown-link-favicon")).toHaveCSS("width", "14px")
const width = await docs.evaluate((link) => link.getBoundingClientRect().width)
release()
await expect(image).toHaveAttribute("data-loaded", "")
await expect(image).toHaveCSS("opacity", "1")
await expect(image).not.toHaveAttribute("role", "button")
expect(await image.evaluate((favicon) => favicon.onclick)).toBeNull()
expect(await docs.evaluate((link) => link.getBoundingClientRect().width)).toBe(width)
expect(
await image.evaluate((favicon) => favicon.getBoundingClientRect().top - favicon.parentElement!.getBoundingClientRect().top),
).toBe(0)
await expect(markdown.getByRole("link", { name: "missing" }).locator(".markdown-link-favicon img")).not.toHaveAttribute(
"data-loaded",
"",
)
await expect(markdown.getByRole("link", { name: "private" }).locator(".markdown-link-favicon img")).toHaveCount(0)
await expect(markdown.getByRole("link", { name: "GitHub" }).locator(".markdown-link-favicon")).toHaveCount(0)
expect(requested).toHaveLength(2)
expect(requested.every((url) => !url.includes("localhost") && !url.includes("github.com"))).toBe(true)
})
async function resolvedColor(page: Page, token: string) {
return page.evaluate((token) => {
const probe = document.createElement("span")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/session-ui",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"license": "MIT",
@@ -16,7 +16,7 @@ export function createImagePreview() {
}
return (root: HTMLElement) => {
root.querySelectorAll<HTMLImageElement>("img").forEach((image) => {
root.querySelectorAll<HTMLImageElement>("img:not([data-markdown-favicon])").forEach((image) => {
image.setAttribute("role", "button")
image.setAttribute("aria-haspopup", "dialog")
image.setAttribute("aria-label", image.alt || i18n.t("ui.imagePreview.alt"))
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.23.49-2.69-.94-2.69-.94-.36-.92-.89-1.17-.89-1.17-.73-.5.06-.49.06-.49.8.06 1.22.82 1.22.82.71 1.21 1.87.86 2.33.66.07-.51.28-.86.51-1.06-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.19 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>

After

Width:  |  Height:  |  Size: 645 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="black">
<circle cx="8" cy="8" r="6"/>
<ellipse cx="8" cy="8" rx="2.5" ry="6"/>
<path d="M2 8h12"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

@@ -101,6 +101,55 @@
text-underline-offset: 2px;
}
a.external-link:is(
[href="https://github.com" i],
[href^="https://github.com/" i],
[href="http://github.com" i],
[href^="http://github.com/" i]
):not(:has(img))::before {
content: "";
display: inline-block;
width: 14px;
height: 14px;
margin-inline-end: 4px;
vertical-align: -0.125em;
background: currentColor;
mask: url("./markdown-github.svg") center / contain no-repeat;
}
.markdown-link-favicon {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
margin-inline-end: 4px;
vertical-align: -0.125em;
}
.markdown-link-favicon::before {
content: "";
position: absolute;
inset: 0;
background: currentColor;
mask: url("./markdown-globe.svg") center / contain no-repeat;
}
.markdown-link-favicon img {
position: absolute;
inset: 0;
display: block;
width: 14px;
height: 14px;
margin: 0;
border-radius: 2px;
cursor: inherit;
opacity: 0;
}
.markdown-link-favicon img[data-loaded] {
opacity: 1;
}
a[data-local-link] {
cursor: pointer;
}
@@ -85,3 +85,42 @@ export const StreamingInlineCode = {
</div>
),
}
const externalLinkChunks = [
"1. ",
"[#540](https://github.com/anomalyco/opencode/pull/540)",
": Batch access-policy reads.\n2. ",
"[Stack Overflow](https://stackoverflow.com/questions/123)",
": Favicon from a public site.\n3. ",
"[MDN docs](https://developer.mozilla.org/en-US/docs/Web)",
": Globe while loading.\n4. ",
"[Unavailable favicon](https://absent-site.example.org/docs)",
": Globe fallback.\n5. ",
"[Local host](http://localhost:8080/docs)",
": No third-party request.",
]
function StreamingExternalLinks() {
const [count, setCount] = createSignal(1)
const timer = setInterval(() => {
setCount((value) => {
if (value === externalLinkChunks.length) {
clearInterval(timer)
return value
}
return value + 1
})
}, 380)
onCleanup(() => clearInterval(timer))
return (
<Markdown text={externalLinkChunks.slice(0, count()).join("")} streaming={count() < externalLinkChunks.length} />
)
}
export const ExternalLinksStreaming = {
render: () => (
<div class="mx-auto max-w-[680px] rounded-lg border border-border-weak-base bg-background-base px-5 py-4">
<StreamingExternalLinks />
</div>
),
}
@@ -289,6 +289,50 @@ function markCodeLinks(root: HTMLDivElement) {
}
}
const publicFaviconHost = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/
const privateFaviconSuffix = /\.(?:alt|example|internal|invalid|local|localhost|onion|test|ts\.net)$/
function markExternalLinkFavicons(root: HTMLDivElement) {
root.querySelectorAll<HTMLAnchorElement>("a.external-link[href]").forEach((link) => {
if (!link.textContent?.trim() || link.querySelector("img")) return
if (!URL.canParse(link.href)) return
const url = new URL(link.href)
if (url.protocol !== "http:" && url.protocol !== "https:") return
if (url.hostname.toLowerCase() === "github.com") return
const favicon = document.createElement("span")
favicon.className = "markdown-link-favicon"
favicon.setAttribute("aria-hidden", "true")
const host = url.hostname.toLowerCase()
if (
publicFaviconHost.test(host) &&
!privateFaviconSuffix.test(host) &&
host !== "home.arpa" &&
!host.endsWith(".home.arpa")
) {
const image = document.createElement("img")
image.src = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(url.host)}&sz=32`
image.alt = ""
image.setAttribute("data-markdown-favicon", "")
image.width = 14
image.height = 14
image.decoding = "async"
favicon.appendChild(image)
}
link.insertBefore(favicon, link.firstChild)
})
}
function setupExternalLinkFavicons(root: HTMLDivElement) {
const loaded = (event: Event) => {
const image = event.target
if (!(image instanceof HTMLImageElement) || !image.hasAttribute("data-markdown-favicon")) return
if (image.naturalWidth > 0) image.dataset.loaded = ""
}
root.addEventListener("load", loaded, true)
return () => root.removeEventListener("load", loaded, true)
}
function markInlineCode(root: HTMLDivElement) {
const codeNodes = Array.from(root.querySelectorAll(":not(pre) > code"))
for (const code of codeNodes) {
@@ -554,6 +598,7 @@ export function Markdown(
let copyCleanup: (() => void) | undefined
let linkCleanup: (() => void) | undefined
let faviconCleanup: (() => void) | undefined
let readImage: ReadMarkdownImage | undefined
let images: ReturnType<typeof createMarkdownImages> | undefined
@@ -599,6 +644,9 @@ export function Markdown(
}
images?.update(container)
previewImages(container)
container.querySelectorAll<HTMLImageElement>("img[data-markdown-favicon]").forEach((image) => {
if (image.complete && image.naturalWidth > 0) image.dataset.loaded = ""
})
container
.querySelectorAll<HTMLElement>('[data-slot="markdown-copy-button"]')
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
@@ -608,6 +656,7 @@ export function Markdown(
copied: i18n.t("ui.message.copied"),
}))
if (!linkCleanup) linkCleanup = setupLocalLinks(container, () => markdown?.openLocalFile)
if (!faviconCleanup) faviconCleanup = setupExternalLinkFavicons(container)
container.toggleAttribute("data-local-links", !!markdown?.openLocalFile)
if (result?.ready && result.text === local.text) container.dataset.markdownReady = ""
})
@@ -617,6 +666,7 @@ export function Markdown(
images?.dispose()
if (copyCleanup) copyCleanup()
if (linkCleanup) linkCleanup()
if (faviconCleanup) faviconCleanup()
const container = root()
if (container) disposeRenderedMarkdown(container)
if (streamed) disposeMarkdownProjection(owner)
@@ -697,6 +747,7 @@ function updateBlock(container: HTMLDivElement, index: number, block: RenderedBl
source.innerHTML = block.html
markInlineCode(source)
markCodeLinks(source)
markExternalLinkFavicons(source)
if (rendered) {
rendered.renderer.update(source.innerHTML, block.mode === "live", rendered.raw !== block.raw)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/simulation",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-app",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-core",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-server",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/theme",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/tui",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/ui",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/util",
"version": "2.0.15",
"version": "2.0.16",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode/web",
"type": "module",
"license": "MIT",
"version": "2.0.15",
"version": "2.0.16",
"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": "2.0.15",
"version": "2.0.16",
"publisher": "sst-dev",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/update",
"version": "2.0.15",
"version": "2.0.16",
"private": true,
"type": "module",
"scripts": {