diff --git a/bun.lock b/bun.lock index 8dae875180..55199e92ae 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@dnd-kit/abstract": "0.5.0", "@dnd-kit/dom": "0.5.0", @@ -93,7 +93,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.14", + "version": "1.17.15", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -141,7 +141,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -155,7 +155,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -191,7 +191,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -218,7 +218,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -240,7 +240,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -264,7 +264,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -284,7 +284,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.14", + "version": "1.17.15", "bin": { "opencode": "./bin/opencode", }, @@ -378,7 +378,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -432,7 +432,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -446,7 +446,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "effect": "catalog:", }, @@ -458,7 +458,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -490,7 +490,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -506,7 +506,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -537,7 +537,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -556,7 +556,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.14", + "version": "1.17.15", "bin": { "codev-code": "./bin/opencode", }, @@ -687,7 +687,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -763,7 +763,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "cross-spawn": "catalog:", }, @@ -778,7 +778,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -793,7 +793,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -837,7 +837,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -850,7 +850,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -883,7 +883,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -902,7 +902,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -943,7 +943,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -970,7 +970,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1021,7 +1021,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 739dfc5ba0..7430a614bf 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.14", + "version": "1.17.15", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 9c2e7d6a1f..f4cf14518c 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -186,7 +186,7 @@ declare global { deepLinks?: string[] } api?: { - setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise + setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise exportDebugLogs?: () => Promise } } @@ -320,8 +320,8 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { { - void window.api?.setTitlebar?.({ mode }) + onThemeApplied={(_, mode, scheme) => { + void window.api?.setTitlebar?.({ mode, scheme }) }} > diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index cea5f34d0d..23857946d0 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -24,6 +24,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip" import { Icon } from "@opencode-ai/ui/v2/icon" import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" import { decode64 } from "@/utils/base64" @@ -92,6 +93,7 @@ const ModelList: Component<{ class="w-full" placement="right-start" gutter={12} + openDelay={0} value={} > {node} @@ -446,26 +448,41 @@ export function ModelSelectorPopoverV2(props: { {(item) => ( - { - setStore("active", modelKey(item)) - setTimeout(() => searchRef?.focus()) - }} - onSelect={() => selectModel(item)} + + } > - {item.name} - - {language.t("model.tag.free")} - - - {language.t("model.tag.latest")} - - + { + setStore("active", modelKey(item)) + setTimeout(() => searchRef?.focus()) + }} + onSelect={() => selectModel(item)} + > + {item.name} + + {language.t("model.tag.free")} + + + {language.t("model.tag.latest")} + + + )} diff --git a/packages/app/src/components/model-tooltip.tsx b/packages/app/src/components/model-tooltip.tsx index 53164dae85..7253c06917 100644 --- a/packages/app/src/components/model-tooltip.tsx +++ b/packages/app/src/components/model-tooltip.tsx @@ -1,4 +1,4 @@ -import { Show, type Component } from "solid-js" +import { Show, type Component, type JSX } from "solid-js" import { useLanguage } from "@/context/language" type InputKey = "text" | "image" | "audio" | "video" | "pdf" @@ -23,7 +23,18 @@ type ModelInfo = { } } -export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => { +function ModelTooltipRow(props: { name: JSX.Element; value: JSX.Element }) { + return ( +
+ {props.name} + {props.value} +
+ ) +} + +export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean; v2?: boolean }> = ( + props, +) => { const language = useLanguage() const sourceName = (model: ModelInfo) => { const value = `${model.id} ${model.name}`.toLowerCase() @@ -51,6 +62,13 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free? const suffix = tags.length ? ` (${tags.join(", ")})` : "" return `${sourceName(props.model)} ${props.model.name}${suffix}` } + const name = () => { + const tags: Array = [] + if (props.latest) tags.push(language.t("model.tag.latest")) + if (props.free) tags.push(language.t("model.tag.free")) + const suffix = tags.length ? ` (${tags.join(", ")})` : "" + return `${props.model.name}${suffix}` + } const inputs = () => { if (props.model.capabilities) { const input = props.model.capabilities.input @@ -73,6 +91,21 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free? : language.t("model.tooltip.reasoning.none") } const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() }) + const contextLimit = () => props.model.limit.context.toLocaleString(language.intl()) + + if (props.v2) { + return ( +
+ + + + {(value) => } + + + +
+ ) + } return (
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index cbbf33d2b1..c424a62a89 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -440,11 +440,6 @@ export const SettingsGeneral: Component = () => { value={(o) => o.value} label={(o) => o.label} onSelect={(option) => option && theme.setColorScheme(option.value)} - onHighlight={(option) => { - if (!option) return - theme.previewColorScheme(option.value) - return () => theme.cancelPreview() - }} variant="secondary" size="small" triggerVariant="settings" @@ -471,11 +466,6 @@ export const SettingsGeneral: Component = () => { if (!option) return theme.setTheme(option.id) }} - onHighlight={(option) => { - if (!option) return - theme.previewTheme(option.id) - return () => theme.cancelPreview() - }} variant="secondary" size="small" triggerVariant="settings" diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx index 0dd48a5523..69dfb77fb0 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -422,11 +422,6 @@ export const SettingsGeneralV2: Component<{ value={(o) => o.value} label={(o) => o.label} onSelect={(option) => option && theme.setColorScheme(option.value)} - onHighlight={(option) => { - if (!option) return - theme.previewColorScheme(option.value) - return () => theme.cancelPreview() - }} /> @@ -454,11 +449,6 @@ export const SettingsGeneralV2: Component<{ if (!option) return theme.setTheme(option.id) }} - onHighlight={(option) => { - if (!option) return - theme.previewTheme(option.id) - return () => theme.cancelPreview() - }} /> diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 7f064932bc..a397046f9b 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -246,18 +246,23 @@ export function TabNavItem(props: { }} class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]" > - - {(session) => ( - + + - )} - + )} + + { titleEl = el diff --git a/packages/app/src/components/titlebar-tab-strip.tsx b/packages/app/src/components/titlebar-tab-strip.tsx index 6705ac1919..7ef53324cd 100644 --- a/packages/app/src/components/titlebar-tab-strip.tsx +++ b/packages/app/src/components/titlebar-tab-strip.tsx @@ -40,6 +40,7 @@ function SessionTabSlot(props: { let ref!: HTMLDivElement const sdk = createMemo(() => props.serverCtx()?.sdk ?? null) const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId)) + const persisted = createMemo(() => tabs.info[props.id]) const [loadedSession] = createResource( () => { const ctx = props.serverCtx() @@ -71,8 +72,10 @@ function SessionTabSlot(props: { createEffect(() => { const value = session() + if (!value) return + tabs.rememberSessionInfo(props.tab, value) const current = sdk() - if (!value || !current) return + if (!current) return createTabPromptState(tabs, props.tab, current.scope, { dir: base64Encode(value.directory), id: value.id, @@ -86,7 +89,7 @@ function SessionTabSlot(props: { data-tab-key={props.id} data-active={props.active()} class="relative flex w-56 min-w-7 max-w-56 flex-shrink" - classList={{ hidden: !session() && !missingSession() }} + classList={{ hidden: !session() && !missingSession() && !persisted()?.title }} > { @@ -95,7 +98,7 @@ function SessionTabSlot(props: { href={tabHref(props.tab)} server={props.tab.server} session={session} - fallbackTitle={missingSession() ? language.t("session.tab.unknown") : undefined} + fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)} onTitleChange={(title) => { const value = session() const ctx = props.serverCtx() diff --git a/packages/app/src/context/command.test.ts b/packages/app/src/context/command.test.ts index 2b956287c5..4297dd89bd 100644 --- a/packages/app/src/context/command.test.ts +++ b/packages/app/src/context/command.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { upsertCommandRegistration } from "./command" +import { resolveKeybindOption, upsertCommandRegistration } from "./command" describe("upsertCommandRegistration", () => { test("replaces keyed registrations", () => { @@ -23,3 +23,19 @@ describe("upsertCommandRegistration", () => { expect(next[1]?.options).toBe(one) }) }) + +describe("resolveKeybindOption", () => { + test("prefers a matching contextual command over the global fallback", () => { + const fallback = { id: "tab.close", title: "Close tab" } + const contextual = { id: "terminal.close", title: "Close terminal", when: () => true } + + expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(contextual) + }) + + test("uses the global fallback outside the command context", () => { + const fallback = { id: "tab.close", title: "Close tab" } + const contextual = { id: "terminal.close", title: "Close terminal", when: () => false } + + expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(fallback) + }) +}) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 0c935307ad..467a1115b5 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -82,10 +82,15 @@ export interface CommandOption { suggested?: boolean disabled?: boolean hidden?: boolean + when?: (event: KeyboardEvent) => boolean onSelect?: (source?: "palette" | "keybind" | "slash") => void onHighlight?: () => (() => void) | void } +export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) { + return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when) +} + type CommandSource = "palette" | "keybind" | "slash" export type CommandCatalogItem = { @@ -334,7 +339,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex }) const keymap = createMemo(() => { - const map = new Map() + const map = new Map() for (const option of options()) { if (option.id.startsWith(SUGGESTED_PREFIX)) continue if (option.disabled) continue @@ -344,8 +349,12 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex for (const kb of keybinds) { if (!kb.key) continue const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt) - if (map.has(sig)) continue - map.set(sig, option) + const existing = map.get(sig) + if (existing) { + existing.push(option) + continue + } + map.set(sig, [option]) } } return map @@ -374,7 +383,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const sig = signatureFromEvent(event) const isPalette = palette().has(sig) - const option = keymap().get(sig) + const option = resolveKeybindOption(keymap().get(sig), event) const modified = event.ctrlKey || event.metaKey || event.altKey const isTab = event.key === "Tab" @@ -383,17 +392,19 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex if (isPalette) { event.preventDefault() + event.stopPropagation() showPalette() return } if (!option) return event.preventDefault() + event.stopPropagation() option.onSelect?.("keybind") } onMount(() => { - makeEventListener(document, "keydown", handleKeyDown) + makeEventListener(document, "keydown", handleKeyDown, { capture: true }) }) function register(cb: () => CommandOption[]): void diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 11155d2ca4..47ed77e781 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -28,6 +28,11 @@ export type DraftTab = { export type Tab = SessionTab | DraftTab +export type TabInfo = { + title?: string + directory?: string +} + type RecentTab = { key?: string } @@ -64,6 +69,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ createStore([]), ) const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore({})) + const [info, setInfo] = persisted(Persist.window("tabs.info"), createStore>({})) const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), createStore([])) const params = useParams() @@ -102,6 +108,15 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform) } + const removeInfo = (key: string) => { + if (!info[key]) return + setInfo( + produce((draft) => { + delete draft[key] + }), + ) + } + onCleanup(memory.dispose) createEffect(() => { @@ -110,11 +125,19 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const next = store.filter((tab) => servers.has(tab.server)) if (next.length !== store.length) { for (const tab of store) { - if (!servers.has(tab.server)) memory.remove(tabKey(tab)) + if (!servers.has(tab.server)) { + const key = tabKey(tab) + memory.remove(key) + removeInfo(key) + } } setStore(() => next) } if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined) + const keys = new Set(next.map(tabKey)) + for (const key of Object.keys(info)) { + if (!keys.has(key)) removeInfo(key) + } }) createEffect(() => { @@ -150,6 +173,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ if (nextTab) navigateTab(nextTab) }).finally(() => closing.delete(key)) memory.remove(key) + removeInfo(key) if (draftID) removeDraftPersisted(draftID) } @@ -264,6 +288,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const removed = store.filter((tab) => tab.server === key).map(tabKey) setStore((tabs) => tabs.filter((tab) => tab.server !== key)) for (const key of removed) memory.remove(key) + for (const key of removed) removeInfo(key) if (recent.key && removed.includes(recent.key)) setRecentKey(undefined) for (const draftID of drafts) removeDraftPersisted(draftID) if (server.key === key) navigate("/") @@ -318,6 +343,14 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ if (recent.key && removed.includes(recent.key)) setRecentKey(undefined) }) for (const key of removed) memory.remove(key) + for (const key of removed) removeInfo(key) + }, + rememberSessionInfo(tab: SessionTab, session: Session) { + const key = tabKey(tab) + const next = { title: session.title, directory: session.directory } + const current = info[key] + if (current?.title === next.title && current.directory === next.directory) return + setInfo(key, next) }, select: navigateTab, remember(tab: Tab) { @@ -342,6 +375,6 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ }, } - return { ...actions, store, ready, recentReady } + return { ...actions, store, info, ready, recentReady } }, }) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 66e60df30e..3da8183bb7 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -212,6 +212,11 @@ export const dict = { "model.input.video": "video", "model.input.pdf": "pdf", "model.tooltip.allows": "Allows: {{inputs}}", + "model.tooltip.context.label": "Context", + "model.tooltip.inputs": "Inputs", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Provider", + "model.tooltip.reasoning": "Reasoning", "model.tooltip.reasoning.allowed": "Allows reasoning", "model.tooltip.reasoning.none": "No reasoning", "model.tooltip.context": "Context limit {{limit}}", diff --git a/packages/app/src/pages/layout/project-avatar-state.ts b/packages/app/src/pages/layout/project-avatar-state.ts index 51a2a28021..ca105af37c 100644 --- a/packages/app/src/pages/layout/project-avatar-state.ts +++ b/packages/app/src/pages/layout/project-avatar-state.ts @@ -2,8 +2,8 @@ import { createMemo, type Accessor } from "solid-js" import { useGlobal } from "@/context/global" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" +import { sessionPermissionRequest, sessionQuestionRequest } from "@/pages/session/composer/session-request-tree" import { ServerConnection } from "@/context/server" -import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree" export function useSessionTabAvatarState( server: Accessor, @@ -26,15 +26,20 @@ export function useSessionTabAvatarState( return !permission.autoResponds(item, directory()) }) }) - const unread = createMemo(() => { - if (hasPermissions()) return true - if (!connection()) return false - return notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0 + const hasQuestions = createMemo(() => { + const serverSync = sync() + if (!serverSync) return false + const [store] = serverSync.child(directory(), { bootstrap: false }) + return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) }) + const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) + const unread = createMemo( + () => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0, + ) const loading = createMemo(() => { const serverSync = sync() if (!serverSync) return false - if (hasPermissions()) return false + if (needsAttention()) return false return serverSync.session.data.session_working(sessionId()) }) return { unread, loading } diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index fd2e29d31c..baa5e817b6 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -269,6 +269,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => { view().terminal.open() } + const closeTerminal = () => { + const id = terminal.active() + if (!id) return + const last = terminal.all().length === 1 + void terminal.close(id) + if (last) view().terminal.close() + } + const chooseMcp = () => { void openDialog( () => import("@/components/dialog-select-mcp"), @@ -518,6 +526,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => { ] const terminalCmds = () => [ + terminalCommand({ + id: "terminal.close", + title: language.t("terminal.close"), + keybind: "mod+w", + hidden: true, + when: (event) => event.target instanceof Element && !!event.target.closest('[data-component="terminal"]'), + onSelect: closeTerminal, + }), terminalCommand({ id: "terminal.new", title: language.t("command.terminal.new"), diff --git a/packages/cli/package.json b/packages/cli/package.json index 5c84ce74ea..eb3bb1bd01 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.17.14", + "version": "1.17.15", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 1830d59c8c..7646ce44b2 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.17.14", + "version": "1.17.15", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 2e3d2342d0..955a6e9162 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.17.14", + "version": "1.17.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 8acc8e14ae..1a2f180c4f 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.17.14", + "version": "1.17.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index c3c8b88945..946e7ddf8d 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.17.14", + "version": "1.17.15", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 26af291437..7977048f5c 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.17.14", + "version": "1.17.15", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 3091274be2..f56e5da413 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.17.14", + "version": "1.17.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index fe6c0950d0..c0927930af 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.14", + "version": "1.17.15", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index 48efe75804..0635aa6291 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -11,6 +11,11 @@ import { FSUtil } from "../../fs-util" import { ModelV2 } from "../../model" import { ConfigAgentV1 } from "../../v1/config/agent" import { ConfigMigrateV1 } from "../../v1/config/migrate" +import { Global } from "../../global" +import { PermissionV2 } from "../../permission" +import type { LocationMutation } from "../../location-mutation" +import type { ReadTool } from "../../tool/read" +import type { EditTool } from "../../tool/edit" const legacySources = [ { pattern: "{agent,agents}/**/*.md", primary: false }, @@ -19,6 +24,11 @@ const legacySources = [ const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info) const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info) const decodeConfig = Schema.decodeUnknownOption(Config.Info) +type PathAction = + | LocationMutation.ExternalDirectoryAuthorization["action"] + | typeof ReadTool.name + | typeof EditTool.name +const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[] const agentKeys = new Set([ "model", "variant", @@ -38,6 +48,7 @@ export const Plugin = define({ effect: Effect.fn(function* (ctx) { const config = yield* Config.Service const fs = yield* FSUtil.Service + const global = yield* Global.Service yield* ctx.agent.transform( Effect.fn(function* (draft) { const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { @@ -56,11 +67,14 @@ export const Plugin = define({ ) }) }).pipe(Effect.map((documents) => documents.flat())) - const global = documents.flatMap((document) => document.info.permissions ?? []) + const permissions = expandPermissions( + documents.flatMap((document) => document.info.permissions ?? []), + global.home, + ) const configuredDefault = Config.latest(documents, "default_agent") if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault)) for (const current of draft.list()) { - draft.update(current.id, (agent) => agent.permissions.push(...global)) + draft.update(current.id, (agent) => agent.permissions.push(...permissions)) } for (const document of documents) { @@ -73,7 +87,7 @@ export const Plugin = define({ const exists = draft.get(agentID) !== undefined draft.update(agentID, (agent) => { - if (!exists) agent.permissions.push(...global) + if (!exists) agent.permissions.push(...permissions) if (item.model !== undefined) { const model = ModelV2.parse(item.model) agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant } @@ -91,7 +105,9 @@ export const Plugin = define({ if (item.hidden !== undefined) agent.hidden = item.hidden if (item.color !== undefined) agent.color = item.color if (item.steps !== undefined) agent.steps = item.steps - if (item.permissions !== undefined) agent.permissions.push(...item.permissions) + if (item.permissions !== undefined) { + agent.permissions.push(...expandPermissions(item.permissions, global.home)) + } }) } } @@ -100,6 +116,27 @@ export const Plugin = define({ }), }) +function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset { + // Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text: + // rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing. + return rules.map((rule) => + isPathAction(rule.action) ? { ...rule, resource: expandHome(rule.resource, home) } : rule, + ) +} + +function isPathAction(action: string): action is PathAction { + return pathActions.some((item) => item === action) +} + +function expandHome(resource: string, home: string) { + if (resource.startsWith("~/")) return home + resource.slice(1) + if (resource === "~") return home + if (resource === "$HOME") return home + if (resource.startsWith("$HOME/")) return home + resource.slice(5) + if (resource.startsWith("$HOME\\")) return home + resource.slice(5) + return resource +} + function discover(fs: FSUtil.Interface, directory: string) { return Effect.forEach(legacySources, (source) => fs diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index ba52eb77dd..4b21ff348f 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -22,20 +22,27 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside Rules: - Keep every section, even when empty. - Use terse bullets, not prose paragraphs. - Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known. -- Put relevant files and symbols inside the section where they matter; do not add extra sections. - Do not mention the summary process or that context was compacted.` type Entry = { diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 4a28fb7877..aa691648a3 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -8,16 +8,43 @@ import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" +import { Global } from "@opencode-ai/core/global" import { PermissionV2 } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" +import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate" import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" import { agentHost, host } from "../plugin/host" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node, Global.node]))) const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigAgentPlugin.Plugin", () => { + it.effect("matches POSIX paths against home-relative permissions", () => + Effect.gen(function* () { + const permissions = yield* loadHomePermissions("/home/test") + expect(PermissionV2.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe( + "allow", + ) + expect(PermissionV2.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny") + expect(PermissionV2.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny") + expect(PermissionV2.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny") + expect(PermissionV2.evaluate("bash", "$HOME/private/key", permissions).effect).toBe("deny") + }), + ) + + it.effect("matches Windows paths against home-relative permissions", () => + Effect.gen(function* () { + const permissions = yield* loadHomePermissions("C:\\Users\\test") + expect( + PermissionV2.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect, + ).toBe("allow") + expect(PermissionV2.evaluate("external_directory", "C:\\Users\\test\\cache\\files\\*", permissions).effect).toBe( + "deny", + ) + }), + ) + it.effect("applies all global permissions before agent-specific permissions", () => Effect.gen(function* () { const agents = yield* AgentV2.Service @@ -272,3 +299,51 @@ Use native v2 fields.`, ), ) }) + +function loadHomePermissions(home: string) { + return Effect.gen(function* () { + const agents = yield* AgentV2.Service + const build = AgentV2.ID.make("build") + yield* agents.transform((editor) => editor.update(build, () => {})) + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode( + ConfigMigrateV1.migrate({ + permission: { + external_directory: { + "~/p/**": "allow", + "/some/~/path": "deny", + "$HOMELESS/**": "deny", + }, + bash: { + "$HOME/private/**": "deny", + }, + }, + agent: { + build: { + permission: { + external_directory: { + "$HOME/cache/**": "deny", + }, + }, + }, + }, + }), + ), + }), + ]), + }) + + yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe( + Effect.provideService(Config.Service, config), + Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home })), + ) + + const agent = yield* agents.get(build) + if (!agent) throw new Error("expected configured build agent") + return agent.permissions + }) +} diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index e91c89c009..9d45e0acc3 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -1,6 +1,15 @@ import { expect, test } from "bun:test" import { SessionCompaction } from "@opencode-ai/core/session/compaction" +test("compaction prompt preserves detailed work state and relevant files", () => { + const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] }) + + expect(prompt).toContain("## Work State\n### Completed") + expect(prompt).toContain("### Active") + expect(prompt).toContain("### Blocked") + expect(prompt).toContain("## Relevant Files") +}) + test("compaction describes tool media without embedding base64", () => { const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" const serialized = SessionCompaction.serializeToolContent([ diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 5703181304..76f2587870 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.17.14", + "version": "1.17.15", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 00c5850e6f..8276eb3b18 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -71,7 +71,10 @@ export function setAppQuitting(quitting = true) { export function setBackgroundColor(color: string) { backgroundColor = color - BrowserWindow.getAllWindows().forEach((win) => win.setBackgroundColor(color)) + BrowserWindow.getAllWindows().forEach((win) => { + win.setBackgroundColor(color) + if (process.platform === "darwin") win.invalidateShadow() + }) } export function getBackgroundColor(): string | undefined { @@ -106,6 +109,13 @@ function overlay(theme: Partial = {}, zoom = 1) { export function setTitlebar(win: BrowserWindow, theme: Partial = {}) { titlebarThemes.set(win, theme) + // macOS draws the window frame hairline and shadow using the NSWindow + // appearance, which follows nativeTheme rather than the rendered content. + // Align it with the app theme so a light app on a dark system does not get + // the dark-appearance border and shadow. A "system" scheme must map to + // "system" (not the resolved mode) or prefers-color-scheme stops tracking + // OS appearance changes in the renderer. + if (process.platform === "darwin") nativeTheme.themeSource = theme.scheme ?? theme.mode ?? "system" updateTitlebar(win) } @@ -172,7 +182,7 @@ export function createMainWindow(id: string = randomUUID()) { ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const, - trafficLightPosition: { x: 12, y: 14 }, + trafficLightPosition: { x: 14, y: 14 }, } : {}), ...(process.platform === "win32" diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index d7509580ad..b57ac83e7f 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -31,6 +31,7 @@ export type UpdaterAPI = { export type LinuxDisplayBackend = "wayland" | "auto" export type TitlebarTheme = { mode: "light" | "dark" + scheme?: "system" | "light" | "dark" } export type FatalRendererError = { error: string diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index cd524eabb5..5b901aaa27 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.14", + "version": "1.17.15", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 936b5d47c6..0f60780033 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.14", + "version": "1.17.15", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 70e12a009e..c319fd05bf 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.17.14", + "version": "1.17.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 1e9b3e5fc1..c3fd3baae2 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.17.14", + "version": "1.17.15", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 0d4f1175ad..ad5248cee5 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.14", + "version": "1.17.15", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index f7371c2938..28b58bd271 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.14", + "version": "1.17.15", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/llm/src/provider-error.ts b/packages/llm/src/provider-error.ts index 8d3f2a8f63..321bd7927e 100644 --- a/packages/llm/src/provider-error.ts +++ b/packages/llm/src/provider-error.ts @@ -6,6 +6,7 @@ const patterns = [ /input is too long for requested model/i, /exceeds the context window/i, /input token count.*exceeds the maximum/i, + /tokens in request more than max tokens allowed/i, /maximum prompt length is \d+/i, /reduce the length of the messages/i, /maximum context length is \d+ tokens/i, diff --git a/packages/llm/test/provider-error.test.ts b/packages/llm/test/provider-error.test.ts new file mode 100644 index 0000000000..3622c89454 --- /dev/null +++ b/packages/llm/test/provider-error.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "bun:test" +import { isContextOverflow } from "../src" + +describe("provider error classification", () => { + test("classifies Z.AI GLM token limit messages as context overflow", () => { + expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true) + }) +}) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ad3786d524..4c1eb0e425 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.14", + "version": "1.17.15", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 1de84c9dd9..9bb688aedd 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1448,6 +1448,7 @@ describe("session.message-v2.fromError", () => { "prompt is too long: 213462 tokens > 200000 maximum", "Your input exceeds the context window of this model", "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)", + "tokens in request more than max tokens allowed", "Please reduce the length of the messages or completion", "400 status code (no body)", "413 status code (no body)", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3069aa104f..f7a00d9704 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.17.14", + "version": "1.17.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index dda5b25445..0b07390618 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.17.14", + "version": "1.17.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index e721d1efdb..855fd297bc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.17.14", + "version": "1.17.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 0419004baa..7443187d68 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.17.14", + "version": "1.17.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/src/components/markdown.css b/packages/session-ui/src/components/markdown.css index 214cc479df..89bf947455 100644 --- a/packages/session-ui/src/components/markdown.css +++ b/packages/session-ui/src/components/markdown.css @@ -121,6 +121,20 @@ margin: 0; } + li:has(> input[type="checkbox"]), + li:has(> p > input[type="checkbox"]) { + position: relative; + list-style-type: none; + } + + li > input[type="checkbox"], + li > p > input[type="checkbox"] { + position: absolute; + top: 4px; + left: -20px; + margin: 0; + } + li > p + p { display: block; margin-top: 0.5rem; diff --git a/packages/slack/package.json b/packages/slack/package.json index 3758c3567a..ccb37497d7 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.17.14", + "version": "1.17.15", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 2c8503d09b..c9df8049da 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.17.14", + "version": "1.17.15", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/app/src/i18n.ts b/packages/stats/app/src/i18n.ts index 5f7597f122..abb9ef49e3 100644 --- a/packages/stats/app/src/i18n.ts +++ b/packages/stats/app/src/i18n.ts @@ -218,17 +218,22 @@ const en = { "model.noUsersTitle": "No user data", "model.noUsersDescription": "No user-bearing rows landed in the current window.", "model.dailyUserChart": "Daily unique user chart", - "model.efficiencyDescription": "Cost, cache behavior, and average OpenCode Go session shape.", + "model.efficiencyDescription": "Cost, cache behavior, and average session shape.", "model.noEfficiencyTitle": "No efficiency data", "model.noEfficiencyDescription": "Efficiency data appears after usage lands.", + "model.totalSpendLabel": "Total spend", "model.cost": "Cost", "model.totalSpend": "total spend", "model.costPerMillion": "Cost / 1M", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", "model.inputOutput": "input / output", "model.observedTokens": "observed all tokens", "model.costSession": "Cost / Session", + "model.averageCostSession": "Average cost / Session", "model.average": "average", "model.tokensSession": "Tokens / Session", + "model.averageTokensSession": "Average tokens / session", "model.cacheRatio": "Cache Ratio", "model.inputTokens": "input tokens", "model.geoDescription": "OpenCode Go model tokens used by country.", diff --git a/packages/stats/app/src/i18n/ar.ts b/packages/stats/app/src/i18n/ar.ts index b1b40f9d8a..b46bf14243 100644 --- a/packages/stats/app/src/i18n/ar.ts +++ b/packages/stats/app/src/i18n/ar.ts @@ -202,6 +202,11 @@ export const dict = { "model.efficiencyDescription": "التكلفة، وسلوك التخزين المؤقت، ومتوسط شكل جلسة OpenCode Go.", "model.noEfficiencyTitle": "لا توجد بيانات كفاءة", "model.noEfficiencyDescription": "تظهر بيانات الكفاءة بعد وصول الاستخدام.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "التكلفة", "model.totalSpend": "إجمالي الإنفاق", "model.costPerMillion": "التكلفة / 1M", diff --git a/packages/stats/app/src/i18n/br.ts b/packages/stats/app/src/i18n/br.ts index fda0f17fb3..24941355c5 100644 --- a/packages/stats/app/src/i18n/br.ts +++ b/packages/stats/app/src/i18n/br.ts @@ -204,6 +204,11 @@ export const dict = { "model.efficiencyDescription": "Custo, comportamento de cache e formato médio da sessão do OpenCode Go.", "model.noEfficiencyTitle": "Sem dados de eficiência", "model.noEfficiencyDescription": "Os dados de eficiência aparecem depois que o uso chega.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Custo", "model.totalSpend": "gasto total", "model.costPerMillion": "Custo / 1M", diff --git a/packages/stats/app/src/i18n/da.ts b/packages/stats/app/src/i18n/da.ts index a6103cd752..35b4ad9972 100644 --- a/packages/stats/app/src/i18n/da.ts +++ b/packages/stats/app/src/i18n/da.ts @@ -203,6 +203,11 @@ export const dict = { "model.efficiencyDescription": "Omkostning, cacheadfærd og gennemsnitlig OpenCode Go-sessionsform.", "model.noEfficiencyTitle": "Ingen effektivitetsdata", "model.noEfficiencyDescription": "Effektivitetsdata vises, når brug lander.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Omkostning", "model.totalSpend": "samlet forbrug", "model.costPerMillion": "Omkostning / 1 mio.", diff --git a/packages/stats/app/src/i18n/de.ts b/packages/stats/app/src/i18n/de.ts index 867d47f624..092d85c8c9 100644 --- a/packages/stats/app/src/i18n/de.ts +++ b/packages/stats/app/src/i18n/de.ts @@ -204,6 +204,11 @@ export const dict = { "model.efficiencyDescription": "Kosten, Cache-Verhalten und durchschnittliche OpenCode Go-Sitzungsform.", "model.noEfficiencyTitle": "Keine Effizienzdaten", "model.noEfficiencyDescription": "Effizienzdaten erscheinen, nachdem Nutzung eingegangen ist.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Kosten", "model.totalSpend": "Gesamtausgaben", "model.costPerMillion": "Kosten / 1 Mio.", diff --git a/packages/stats/app/src/i18n/es.ts b/packages/stats/app/src/i18n/es.ts index db7e74552c..a326d913f0 100644 --- a/packages/stats/app/src/i18n/es.ts +++ b/packages/stats/app/src/i18n/es.ts @@ -203,6 +203,11 @@ export const dict = { "model.efficiencyDescription": "Coste, comportamiento de caché y forma media de sesión de OpenCode Go.", "model.noEfficiencyTitle": "Sin datos de eficiencia", "model.noEfficiencyDescription": "Los datos de eficiencia aparecen después de que llegue uso.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Coste", "model.totalSpend": "gasto total", "model.costPerMillion": "Coste / 1 M", diff --git a/packages/stats/app/src/i18n/fr.ts b/packages/stats/app/src/i18n/fr.ts index a69b5c782b..fc0c103495 100644 --- a/packages/stats/app/src/i18n/fr.ts +++ b/packages/stats/app/src/i18n/fr.ts @@ -205,6 +205,11 @@ export const dict = { "model.efficiencyDescription": "Coût, comportement du cache et forme moyenne des sessions OpenCode Go.", "model.noEfficiencyTitle": "Aucune donnée d'efficacité", "model.noEfficiencyDescription": "Les données d'efficacité apparaissent après l'arrivée de l'utilisation.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Coût", "model.totalSpend": "dépense totale", "model.costPerMillion": "Coût / 1 M", diff --git a/packages/stats/app/src/i18n/it.ts b/packages/stats/app/src/i18n/it.ts index 94f5d67d1c..83c39f17bb 100644 --- a/packages/stats/app/src/i18n/it.ts +++ b/packages/stats/app/src/i18n/it.ts @@ -204,6 +204,11 @@ export const dict = { "model.efficiencyDescription": "Costi, comportamento della cache e forma media della sessione OpenCode Go.", "model.noEfficiencyTitle": "Nessun dato di efficienza", "model.noEfficiencyDescription": "I dati di efficienza appaiono dopo l'arrivo dell'utilizzo.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Costo", "model.totalSpend": "spesa totale", "model.costPerMillion": "Costo / 1 M", diff --git a/packages/stats/app/src/i18n/ja.ts b/packages/stats/app/src/i18n/ja.ts index 79957d2f0d..ddd577cfc3 100644 --- a/packages/stats/app/src/i18n/ja.ts +++ b/packages/stats/app/src/i18n/ja.ts @@ -205,6 +205,11 @@ export const dict = { "model.efficiencyDescription": "コスト、キャッシュ動作、平均的なOpenCode Goセッションの形。", "model.noEfficiencyTitle": "効率データがありません", "model.noEfficiencyDescription": "使用量が届くと効率データが表示されます。", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "コスト", "model.totalSpend": "総支出", "model.costPerMillion": "100万あたりのコスト", diff --git a/packages/stats/app/src/i18n/ko.ts b/packages/stats/app/src/i18n/ko.ts index cf22c5db19..8a5d93dca0 100644 --- a/packages/stats/app/src/i18n/ko.ts +++ b/packages/stats/app/src/i18n/ko.ts @@ -205,6 +205,11 @@ export const dict = { "model.efficiencyDescription": "비용, 캐시 동작, 평균 OpenCode Go 세션 형태입니다.", "model.noEfficiencyTitle": "효율 데이터 없음", "model.noEfficiencyDescription": "사용량이 들어오면 효율 데이터가 표시됩니다.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "비용", "model.totalSpend": "총 지출", "model.costPerMillion": "100만당 비용", diff --git a/packages/stats/app/src/i18n/no.ts b/packages/stats/app/src/i18n/no.ts index 62399d3bcd..2c6d957810 100644 --- a/packages/stats/app/src/i18n/no.ts +++ b/packages/stats/app/src/i18n/no.ts @@ -203,6 +203,11 @@ export const dict = { "model.efficiencyDescription": "Kostnad, cacheatferd og gjennomsnittlig OpenCode Go-øktform.", "model.noEfficiencyTitle": "Ingen effektivitetsdata", "model.noEfficiencyDescription": "Effektivitetsdata vises etter at bruk lander.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Kostnad", "model.totalSpend": "totalt forbruk", "model.costPerMillion": "Kostnad / 1 mill.", diff --git a/packages/stats/app/src/i18n/pl.ts b/packages/stats/app/src/i18n/pl.ts index 61267a0c3c..d61f40cda3 100644 --- a/packages/stats/app/src/i18n/pl.ts +++ b/packages/stats/app/src/i18n/pl.ts @@ -202,6 +202,11 @@ export const dict = { "model.efficiencyDescription": "Koszt, zachowanie cache i średni kształt sesji OpenCode Go.", "model.noEfficiencyTitle": "Brak danych wydajności", "model.noEfficiencyDescription": "Dane wydajności pojawią się po nadejściu użycia.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Koszt", "model.totalSpend": "łączne wydatki", "model.costPerMillion": "Koszt / 1 mln", diff --git a/packages/stats/app/src/i18n/ru.ts b/packages/stats/app/src/i18n/ru.ts index 0129c4e896..d0bfeacc84 100644 --- a/packages/stats/app/src/i18n/ru.ts +++ b/packages/stats/app/src/i18n/ru.ts @@ -204,6 +204,11 @@ export const dict = { "model.efficiencyDescription": "Стоимость, поведение кэша и средняя форма сеанса OpenCode Go.", "model.noEfficiencyTitle": "Нет данных эффективности", "model.noEfficiencyDescription": "Данные эффективности появятся после использования.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Стоимость", "model.totalSpend": "общие расходы", "model.costPerMillion": "Стоимость / 1 млн", diff --git a/packages/stats/app/src/i18n/th.ts b/packages/stats/app/src/i18n/th.ts index e75f0b7603..59733d5c6c 100644 --- a/packages/stats/app/src/i18n/th.ts +++ b/packages/stats/app/src/i18n/th.ts @@ -204,6 +204,11 @@ export const dict = { "model.efficiencyDescription": "ต้นทุน พฤติกรรมแคช และรูปแบบเซสชัน OpenCode Go โดยเฉลี่ย", "model.noEfficiencyTitle": "ไม่มีข้อมูลประสิทธิภาพ", "model.noEfficiencyDescription": "ข้อมูลประสิทธิภาพจะแสดงหลังจากมีการใช้งานเข้ามา", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "ต้นทุน", "model.totalSpend": "ยอดใช้จ่ายรวม", "model.costPerMillion": "ต้นทุน / 1 ล้าน", diff --git a/packages/stats/app/src/i18n/tr.ts b/packages/stats/app/src/i18n/tr.ts index 1a5a3455e5..a9961943ea 100644 --- a/packages/stats/app/src/i18n/tr.ts +++ b/packages/stats/app/src/i18n/tr.ts @@ -204,6 +204,11 @@ export const dict = { "model.efficiencyDescription": "Maliyet, önbellek davranışı ve ortalama OpenCode Go oturum yapısı.", "model.noEfficiencyTitle": "Verimlilik verisi yok", "model.noEfficiencyDescription": "Verimlilik verileri kullanım geldikten sonra görünür.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Maliyet", "model.totalSpend": "toplam harcama", "model.costPerMillion": "Maliyet / 1M", diff --git a/packages/stats/app/src/i18n/uk.ts b/packages/stats/app/src/i18n/uk.ts index 2c089ec0dd..99d53acf92 100644 --- a/packages/stats/app/src/i18n/uk.ts +++ b/packages/stats/app/src/i18n/uk.ts @@ -204,6 +204,11 @@ export const dict = { "model.efficiencyDescription": "Вартість, поведінка кешу та середня форма сеансу OpenCode Go.", "model.noEfficiencyTitle": "Немає даних ефективності", "model.noEfficiencyDescription": "Дані ефективності з'являться після використання.", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "Вартість", "model.totalSpend": "загальні витрати", "model.costPerMillion": "Вартість / 1 млн", diff --git a/packages/stats/app/src/i18n/zh.ts b/packages/stats/app/src/i18n/zh.ts index d048b40107..7ff477c97f 100644 --- a/packages/stats/app/src/i18n/zh.ts +++ b/packages/stats/app/src/i18n/zh.ts @@ -203,6 +203,11 @@ export const dict = { "model.efficiencyDescription": "成本、缓存行为和平均 OpenCode Go 会话形态。", "model.noEfficiencyTitle": "无效率数据", "model.noEfficiencyDescription": "使用量到达后会显示效率数据。", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "成本", "model.totalSpend": "总支出", "model.costPerMillion": "每 100 万成本", diff --git a/packages/stats/app/src/i18n/zht.ts b/packages/stats/app/src/i18n/zht.ts index 854ae01be6..dca9ade4db 100644 --- a/packages/stats/app/src/i18n/zht.ts +++ b/packages/stats/app/src/i18n/zht.ts @@ -203,6 +203,11 @@ export const dict = { "model.efficiencyDescription": "成本、快取行為和平均 OpenCode Go 工作階段形態。", "model.noEfficiencyTitle": "無效率數據", "model.noEfficiencyDescription": "使用量到達後會顯示效率數據。", + "model.totalSpendLabel": "Total spend", + "model.costInput": "Cost input", + "model.costOutput": "Cost output", + "model.averageCostSession": "Average cost / Session", + "model.averageTokensSession": "Average tokens / session", "model.cost": "成本", "model.totalSpend": "總支出", "model.costPerMillion": "每 100 萬成本", diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx index e7f32681b7..a71fd8e61b 100644 --- a/packages/stats/app/src/routes/[lab]/[model].tsx +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -28,7 +28,6 @@ import { formatCatalogLabName, getModelCatalog, type ModelCatalog, - type ModelCatalogCost, type ModelCatalogEntry, } from "../model-catalog" import { SectionHeading } from "../section-heading" @@ -881,35 +880,32 @@ function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: M } > {(data) => ( -
- - - - - -
+ <> +