Compare commits

..

2 Commits

Author SHA1 Message Date
usrnk1 43369bd9fc chore(app): translate composer add menu strings 2026-07-07 13:31:43 +02:00
usrnk1 8f65610c15 feat(app): add composer add menu with draft-preserving commands
Convert the composer plus button into a menu offering Images and files,
Commands, Context, and Shell command. Each entry reuses the existing
attachment, slash, at, and shell flows.

When Commands is opened with an empty composer it inserts / and filters
inline as before. With an existing draft it opens a dedicated command
search field so the draft is preserved: built-in commands run against it
and custom commands prefix it as arguments.
2026-07-07 13:31:37 +02:00
84 changed files with 356 additions and 512 deletions
+28 -28
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.17.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -141,7 +141,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -155,7 +155,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.17.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"bin": {
"opencode": "./bin/opencode",
},
@@ -378,7 +378,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.17.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -446,7 +446,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"effect": "catalog:",
},
@@ -458,7 +458,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -490,7 +490,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.17.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -556,7 +556,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.17.15",
"version": "1.17.14",
"bin": {
"opencode": "./bin/opencode",
},
@@ -687,7 +687,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.17.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -778,7 +778,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -793,7 +793,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -837,7 +837,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.17.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"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.15",
"version": "1.17.14",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -970,7 +970,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1021,7 +1021,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.17.15",
"version": "1.17.14",
"description": "",
"type": "module",
"exports": {
@@ -86,7 +86,6 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
const state = { cleanup: undefined as (() => void) | void, committed: false }
const filesOnly = () => props.filesOnly?.() ?? false
const filesEnabled = () => filesOnly() || layout.route().type === "session"
const allowedCommands = createMemo(() => {
if (filesOnly()) return []
@@ -115,7 +114,6 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
})
const recentFileEntries = createMemo(() => {
if (!filesEnabled()) return []
const all = tabState.openedTabs()
const active = tabState.activeFileTab()
const order = active ? [active, ...all.filter((item) => item !== active)] : all
@@ -132,7 +130,6 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
.map((path) => createCommandPaletteFileEntry(path, category))
})
const rootFileEntries = createMemo(() => {
if (!filesEnabled()) return []
const category = language.t("palette.group.files")
return file.tree
.children("")
@@ -214,7 +211,6 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
preferredCommandEntries,
recentFileEntries,
rootFileEntries,
searchFiles: (query: string) => (filesEnabled() ? file.searchFiles(query) : Promise.resolve([])),
sessions,
highlight,
select,
@@ -37,7 +37,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
const q = text.trim()
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
const [files, nextSessions] = await Promise.all([palette.searchFiles(q), Promise.resolve(palette.sessions(q))])
const [files, nextSessions] = await Promise.all([palette.file.searchFiles(q), Promise.resolve(palette.sessions(q))])
const category = palette.language.t("palette.group.files")
return [
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
@@ -92,13 +92,13 @@ function DialogSelectFileLegacy(props: { filesOnly: () => boolean; onOpenFile?:
if (!query) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
if (props.filesOnly()) {
const files = await palette.searchFiles(query)
const files = await palette.file.searchFiles(query)
const category = palette.language.t("palette.group.files")
return files.map((path) => createCommandPaletteFileEntry(path, category))
}
const [files, nextSessions] = await Promise.all([
palette.searchFiles(query),
palette.file.searchFiles(query),
Promise.resolve(palette.sessions(query)),
])
const category = palette.language.t("palette.group.files")
+110 -20
View File
@@ -520,7 +520,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const setMode = (mode: "normal" | "shell") => {
setStore("mode", mode)
setStore("popover", null)
setStore({ popover: null, slashMenu: false, slashMenuQuery: "" })
requestAnimationFrame(() => editorRef?.focus())
}
@@ -554,7 +554,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
},
])
const closePopover = () => setStore("popover", null)
const closePopover = () => setStore({ popover: null, slashMenu: false, slashMenuQuery: "" })
const resetHistoryNavigation = (force = false) => {
if (!force && (store.historyIndex < 0 || store.applyingHistory)) return
@@ -800,17 +800,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
if (!cmd) return
const menu = store.slashMenu
closePopover()
const images = imageAttachments()
if (cmd.type === "custom") {
const text = `/${cmd.trigger} `
if (menu) {
editorRef.focus()
setCursorPosition(editorRef, 0)
addPart({ type: "text", content: text, start: 0, end: text.length })
focusEditorEnd()
return
}
setEditorText(text)
prompt.set([{ type: "text", content: text, start: 0, end: text.length }, ...images], text.length)
focusEditorEnd()
return
}
if (menu) {
command.trigger(cmd.id, "slash")
return
}
clearEditor()
prompt.set([...DEFAULT_PROMPT, ...images], 0)
command.trigger(cmd.id, "slash")
@@ -1072,10 +1085,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (atMatch) {
atOnInput(atMatch[1])
setStore("popover", "at")
setStore({ popover: "at", slashMenu: false, slashMenuQuery: "" })
} else if (slashMatch) {
slashOnInput(slashMatch[1])
setStore("popover", "slash")
setStore({ popover: "slash", slashMenu: false, slashMenuQuery: "" })
} else {
closePopover()
}
@@ -1171,6 +1184,28 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return true
}
const openCommands = () => {
const populated = prompt.dirty() || commentCount() > 0
requestAnimationFrame(() => {
if (!populated) {
if (!addPart({ type: "text", content: "/", start: 0, end: 0 })) return
slashOnInput("")
setStore({ popover: "slash", slashMenu: false, slashMenuQuery: "" })
return
}
slashOnInput("")
setStore({ popover: "slash", slashMenu: true, slashMenuQuery: "" })
})
}
const openContext = () => {
requestAnimationFrame(() => {
if (!addPart({ type: "text", content: "@", start: 0, end: 0 })) return
atOnInput("")
setStore({ popover: "at", slashMenu: false, slashMenuQuery: "" })
})
}
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
history.add(prompt, mode, mode === "shell" ? [] : historyComments())
}
@@ -1199,7 +1234,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
setStore("mode", "normal")
setStore("popover", null)
closePopover()
setStore("historyIndex", -1)
setStore("savedPrompt", null)
prompt.set(edit.prompt, promptLength(edit.prompt))
@@ -1286,7 +1321,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
resetHistoryNavigation(true)
},
setMode: (mode) => setStore("mode", mode),
setPopover: (popover) => setStore("popover", popover),
setPopover: (popover) => {
if (!popover) return closePopover()
setStore({ popover, slashMenu: false, slashMenuQuery: "" })
},
newSessionWorktree: () => props.newSessionWorktree,
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
shouldQueue: props.shouldQueue,
@@ -1325,7 +1363,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const cursorPosition = getCursorPosition(editorRef)
if (cursorPosition === 0) {
setStore("mode", "shell")
setStore("popover", null)
closePopover()
event.preventDefault()
return
}
@@ -1460,6 +1498,29 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const handleSlashMenuKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closePopover()
requestAnimationFrame(() => editorRef.focus())
event.preventDefault()
return
}
if (event.key === "Tab") {
selectPopoverActive()
event.preventDefault()
return
}
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter"
const ctrlNav = ctrl && (event.key === "n" || event.key === "p")
if (!nav && !ctrlNav) return
slashOnKeyDown(event)
if (event.key === "ArrowUp" || event.key === "ArrowDown" || ctrlNav) scrollSlashActiveIntoView()
event.preventDefault()
}
const agentsLoading = () => props.controls.agents.loading
const agentsShouldFadeIn = createMemo<boolean>((prev) => prev ?? agentsLoading())
const providersLoading = () => props.controls.model.loading
@@ -1527,6 +1588,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
slashActive={slashActive() ?? undefined}
setSlashActive={setSlashActive}
onSlashSelect={handleSlashSelect}
slashMenu={store.slashMenu}
slashMenuQuery={store.slashMenuQuery}
onSlashMenuInput={(value) => {
setStore("slashMenuQuery", value)
slashOnInput(value)
}}
onSlashMenuKeyDown={handleSlashMenuKeyDown}
commandKeybind={command.keybind}
commandKeybindParts={command.keybindParts}
newLayoutDesigns={props.controls.newLayoutDesigns}
@@ -1625,23 +1693,45 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
placement="top"
value={
<>
{language.t("prompt.action.attachFile")}
{language.t("prompt.menu.addImagesAndFiles")}
<KeybindV2 keys={command.keybindParts("file.attach")} variant="neutral" />
</>
}
>
<IconButton
data-action="prompt-attach"
type="button"
icon="plus"
variant="ghost"
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted"
style={buttons()}
onClick={pick}
disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.action.attachFile")}
/>
<MenuV2 gutter={6} modal={false} placement="top-start">
<MenuV2.Trigger
as={IconButton}
data-action="prompt-attach"
type="button"
icon="plus"
variant="ghost"
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted"
style={buttons()}
disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.menu.addImagesAndFiles")}
/>
<MenuV2.Portal>
<MenuV2.Content
class="[&_[data-slot=menu-v2-item-shortcut]]:w-5 [&_[data-slot=menu-v2-item-shortcut]]:justify-center"
style={{ "min-width": "180px" }}
>
<MenuV2.Item onSelect={pick} shortcut={command.keybind("file.attach")}>
{language.t("prompt.menu.imagesAndFiles")}
</MenuV2.Item>
<MenuV2.Separator />
<MenuV2.Item onSelect={openCommands} shortcut="/">
{language.t("prompt.menu.commands")}
</MenuV2.Item>
<MenuV2.Item onSelect={openContext} shortcut="@">
{language.t("prompt.menu.context")}
</MenuV2.Item>
<MenuV2.Item onSelect={() => setMode("shell")} shortcut="!">
{language.t("prompt.menu.shellCommand")}
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</TooltipV2>
<Show when={showAgentControl()}>
<ComposerAgentControl state={agentControlState()} />
@@ -41,6 +41,10 @@ type PromptPopoverProps = {
slashActive?: string
setSlashActive: (id: string) => void
onSlashSelect: (item: SlashCommand) => void
slashMenu: boolean
slashMenuQuery: string
onSlashMenuInput: (value: string) => void
onSlashMenuKeyDown: (event: KeyboardEvent) => void
commandKeybind: (id: string) => string | undefined
commandKeybindParts: (id: string) => string[]
newLayoutDesigns: boolean
@@ -254,6 +258,20 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
</Show>
</Match>
<Match when={props.popover === "slash"}>
<Show when={props.slashMenu}>
<div class="px-2 py-1">
<input
ref={(el) => requestAnimationFrame(() => el.focus())}
value={props.slashMenuQuery}
onInput={(event) => props.onSlashMenuInput(event.currentTarget.value)}
onKeyDown={props.onSlashMenuKeyDown}
onMouseDown={(event) => event.stopPropagation()}
aria-label={props.t("prompt.menu.commands")}
placeholder="/"
class="w-full bg-transparent outline-none text-[13px] leading-5 text-v2-text-text-base placeholder:text-v2-text-text-faint"
/>
</div>
</Show>
<Show
when={props.slashFlat.length > 0}
fallback={
@@ -4,6 +4,8 @@ import type { PromptHistoryEntry } from "./history"
export type PromptInputTransientState = {
popover: "at" | "slash" | null
slashMenu: boolean
slashMenuQuery: string
historyIndex: number
savedPrompt: PromptHistoryEntry | null
placeholder: number
@@ -16,6 +18,8 @@ export type PromptInputTransientState = {
function resetPromptInputTransientState(setStore: SetStoreFunction<PromptInputTransientState>) {
setStore({
popover: null,
slashMenu: false,
slashMenuQuery: "",
historyIndex: -1,
savedPrompt: null,
draggingType: null,
@@ -28,6 +32,8 @@ function resetPromptInputTransientState(setStore: SetStoreFunction<PromptInputTr
export function createPromptInputTransientState(identity: Accessor<unknown>, placeholder: number) {
const [store, setStore] = createStore<PromptInputTransientState>({
popover: null,
slashMenu: false,
slashMenuQuery: "",
historyIndex: -1,
savedPrompt: null,
placeholder,
+5
View File
@@ -255,6 +255,11 @@ export const dict = {
"prompt.context.removeActiveFile": "إزالة الملف النشط من السياق",
"prompt.context.removeFile": "إزالة الملف من السياق",
"prompt.action.attachFile": "إرفاق ملف",
"prompt.menu.addImagesAndFiles": "إضافة ملفات والمزيد",
"prompt.menu.imagesAndFiles": "الصور والملفات",
"prompt.menu.commands": "الأوامر",
"prompt.menu.context": "السياق",
"prompt.menu.shellCommand": "أمر shell",
"prompt.attachment.remove": "إزالة المرفق",
"prompt.action.send": "إرسال",
"prompt.action.stop": "توقف",
+5
View File
@@ -255,6 +255,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Remover arquivo ativo do contexto",
"prompt.context.removeFile": "Remover arquivo do contexto",
"prompt.action.attachFile": "Anexar arquivo",
"prompt.menu.addImagesAndFiles": "Adicionar arquivos e mais",
"prompt.menu.imagesAndFiles": "Imagens e arquivos",
"prompt.menu.commands": "Comandos",
"prompt.menu.context": "Contexto",
"prompt.menu.shellCommand": "Comando shell",
"prompt.attachment.remove": "Remover anexo",
"prompt.action.send": "Enviar",
"prompt.action.stop": "Parar",
+5
View File
@@ -275,6 +275,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Ukloni aktivnu datoteku iz konteksta",
"prompt.context.removeFile": "Ukloni datoteku iz konteksta",
"prompt.action.attachFile": "Priloži datoteku",
"prompt.menu.addImagesAndFiles": "Dodaj datoteke i više",
"prompt.menu.imagesAndFiles": "Slike i datoteke",
"prompt.menu.commands": "Komande",
"prompt.menu.context": "Kontekst",
"prompt.menu.shellCommand": "Shell naredba",
"prompt.attachment.remove": "Ukloni prilog",
"prompt.action.send": "Pošalji",
"prompt.action.stop": "Zaustavi",
+5
View File
@@ -273,6 +273,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst",
"prompt.context.removeFile": "Fjern fil fra kontekst",
"prompt.action.attachFile": "Vedhæft fil",
"prompt.menu.addImagesAndFiles": "Tilføj filer og mere",
"prompt.menu.imagesAndFiles": "Billeder og filer",
"prompt.menu.commands": "Kommandoer",
"prompt.menu.context": "Kontekst",
"prompt.menu.shellCommand": "Shell-kommando",
"prompt.attachment.remove": "Fjern vedhæftning",
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
+5
View File
@@ -260,6 +260,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Aktive Datei aus dem Kontext entfernen",
"prompt.context.removeFile": "Datei aus dem Kontext entfernen",
"prompt.action.attachFile": "Datei anhängen",
"prompt.menu.addImagesAndFiles": "Dateien und mehr hinzufügen",
"prompt.menu.imagesAndFiles": "Bilder und Dateien",
"prompt.menu.commands": "Befehle",
"prompt.menu.context": "Kontext",
"prompt.menu.shellCommand": "Shell-Befehl",
"prompt.attachment.remove": "Anhang entfernen",
"prompt.action.send": "Senden",
"prompt.action.stop": "Stopp",
+5
View File
@@ -287,6 +287,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Remove active file from context",
"prompt.context.removeFile": "Remove file from context",
"prompt.action.attachFile": "Add files",
"prompt.menu.addImagesAndFiles": "Add files and more",
"prompt.menu.imagesAndFiles": "Images and files",
"prompt.menu.commands": "Commands",
"prompt.menu.context": "Context",
"prompt.menu.shellCommand": "Shell command",
"prompt.attachment.remove": "Remove attachment",
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
+5
View File
@@ -274,6 +274,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Eliminar archivo activo del contexto",
"prompt.context.removeFile": "Eliminar archivo del contexto",
"prompt.action.attachFile": "Adjuntar archivo",
"prompt.menu.addImagesAndFiles": "Añadir archivos y más",
"prompt.menu.imagesAndFiles": "Imágenes y archivos",
"prompt.menu.commands": "Comandos",
"prompt.menu.context": "Contexto",
"prompt.menu.shellCommand": "Comando de shell",
"prompt.attachment.remove": "Eliminar adjunto",
"prompt.action.send": "Enviar",
"prompt.action.stop": "Detener",
+5
View File
@@ -255,6 +255,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Retirer le fichier actif du contexte",
"prompt.context.removeFile": "Retirer le fichier du contexte",
"prompt.action.attachFile": "Joindre un fichier",
"prompt.menu.addImagesAndFiles": "Ajouter des fichiers et plus encore",
"prompt.menu.imagesAndFiles": "Images et fichiers",
"prompt.menu.commands": "Commandes",
"prompt.menu.context": "Contexte",
"prompt.menu.shellCommand": "Commande shell",
"prompt.attachment.remove": "Supprimer la pièce jointe",
"prompt.action.send": "Envoyer",
"prompt.action.stop": "Arrêter",
+5
View File
@@ -254,6 +254,11 @@ export const dict = {
"prompt.context.removeActiveFile": "コンテキストからアクティブなファイルを削除",
"prompt.context.removeFile": "コンテキストからファイルを削除",
"prompt.action.attachFile": "ファイルを添付",
"prompt.menu.addImagesAndFiles": "ファイルなどを追加",
"prompt.menu.imagesAndFiles": "画像とファイル",
"prompt.menu.commands": "コマンド",
"prompt.menu.context": "コンテキスト",
"prompt.menu.shellCommand": "シェルコマンド",
"prompt.attachment.remove": "添付ファイルを削除",
"prompt.action.send": "送信",
"prompt.action.stop": "停止",
+5
View File
@@ -254,6 +254,11 @@ export const dict = {
"prompt.context.removeActiveFile": "컨텍스트에서 활성 파일 제거",
"prompt.context.removeFile": "컨텍스트에서 파일 제거",
"prompt.action.attachFile": "파일 첨부",
"prompt.menu.addImagesAndFiles": "파일 및 기타 항목 추가",
"prompt.menu.imagesAndFiles": "이미지 및 파일",
"prompt.menu.commands": "명령어",
"prompt.menu.context": "컨텍스트",
"prompt.menu.shellCommand": "셸 명령",
"prompt.attachment.remove": "첨부 파일 제거",
"prompt.action.send": "전송",
"prompt.action.stop": "중지",
+5
View File
@@ -277,6 +277,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst",
"prompt.context.removeFile": "Fjern fil fra kontekst",
"prompt.action.attachFile": "Legg ved fil",
"prompt.menu.addImagesAndFiles": "Legg til filer og mer",
"prompt.menu.imagesAndFiles": "Bilder og filer",
"prompt.menu.commands": "Kommandoer",
"prompt.menu.context": "Kontekst",
"prompt.menu.shellCommand": "Shell-kommando",
"prompt.attachment.remove": "Fjern vedlegg",
"prompt.action.send": "Send",
"prompt.action.stop": "Stopp",
+5
View File
@@ -256,6 +256,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Usuń aktywny plik z kontekstu",
"prompt.context.removeFile": "Usuń plik z kontekstu",
"prompt.action.attachFile": "Załącz plik",
"prompt.menu.addImagesAndFiles": "Dodaj pliki i inne elementy",
"prompt.menu.imagesAndFiles": "Obrazy i pliki",
"prompt.menu.commands": "Polecenia",
"prompt.menu.context": "Kontekst",
"prompt.menu.shellCommand": "Polecenie powłoki",
"prompt.attachment.remove": "Usuń załącznik",
"prompt.action.send": "Wyślij",
"prompt.action.stop": "Zatrzymaj",
+5
View File
@@ -274,6 +274,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Удалить активный файл из контекста",
"prompt.context.removeFile": "Удалить файл из контекста",
"prompt.action.attachFile": "Прикрепить файл",
"prompt.menu.addImagesAndFiles": "Добавить файлы и другое",
"prompt.menu.imagesAndFiles": "Изображения и файлы",
"prompt.menu.commands": "Команды",
"prompt.menu.context": "Контекст",
"prompt.menu.shellCommand": "Команда оболочки",
"prompt.attachment.remove": "Удалить вложение",
"prompt.action.send": "Отправить",
"prompt.action.stop": "Остановить",
+5
View File
@@ -274,6 +274,11 @@ export const dict = {
"prompt.context.removeActiveFile": "เอาไฟล์ที่ใช้งานอยู่ออกจากบริบท",
"prompt.context.removeFile": "เอาไฟล์ออกจากบริบท",
"prompt.action.attachFile": "แนบไฟล์",
"prompt.menu.addImagesAndFiles": "เพิ่มไฟล์และอื่น ๆ",
"prompt.menu.imagesAndFiles": "รูปภาพและไฟล์",
"prompt.menu.commands": "คำสั่ง",
"prompt.menu.context": "บริบท",
"prompt.menu.shellCommand": "คำสั่งเชลล์",
"prompt.attachment.remove": "เอาไฟล์แนบออก",
"prompt.action.send": "ส่ง",
"prompt.action.stop": "หยุด",
+5
View File
@@ -279,6 +279,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Aktif dosyayı bağlamdan çıkar",
"prompt.context.removeFile": "Dosyayı bağlamdan çıkar",
"prompt.action.attachFile": "Dosya ekle",
"prompt.menu.addImagesAndFiles": "Dosya ve daha fazlasını ekle",
"prompt.menu.imagesAndFiles": "Görseller ve dosyalar",
"prompt.menu.commands": "Komutlar",
"prompt.menu.context": "Bağlam",
"prompt.menu.shellCommand": "Kabuk komutu",
"prompt.attachment.remove": "Eki kaldır",
"prompt.action.send": "Gönder",
"prompt.action.stop": "Durdur",
+5
View File
@@ -281,6 +281,11 @@ export const dict = {
"prompt.context.removeActiveFile": "Видалити активний файл з контексту",
"prompt.context.removeFile": "Видалити файл з контексту",
"prompt.action.attachFile": "Додати файли",
"prompt.menu.addImagesAndFiles": "Додати файли та інше",
"prompt.menu.imagesAndFiles": "Зображення та файли",
"prompt.menu.commands": "Команди",
"prompt.menu.context": "Контекст",
"prompt.menu.shellCommand": "Команда оболонки",
"prompt.attachment.remove": "Видалити вкладення",
"prompt.action.send": "Надіслати",
"prompt.action.stop": "Зупинити",
+5
View File
@@ -294,6 +294,11 @@ export const dict = {
"prompt.context.removeActiveFile": "从上下文移除活动文件",
"prompt.context.removeFile": "从上下文移除文件",
"prompt.action.attachFile": "附加文件",
"prompt.menu.addImagesAndFiles": "添加文件及更多内容",
"prompt.menu.imagesAndFiles": "图片和文件",
"prompt.menu.commands": "命令",
"prompt.menu.context": "上下文",
"prompt.menu.shellCommand": "shell 命令",
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "发送",
"prompt.action.stop": "停止",
+5
View File
@@ -274,6 +274,11 @@ export const dict = {
"prompt.context.removeActiveFile": "從上下文移除目前檔案",
"prompt.context.removeFile": "從上下文移除檔案",
"prompt.action.attachFile": "附加檔案",
"prompt.menu.addImagesAndFiles": "新增檔案及更多內容",
"prompt.menu.imagesAndFiles": "圖片和檔案",
"prompt.menu.commands": "命令",
"prompt.menu.context": "上下文",
"prompt.menu.shellCommand": "shell 命令",
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "傳送",
"prompt.action.stop": "停止",
+1 -26
View File
@@ -1,9 +1,8 @@
import { Show, createEffect, createMemo, createResource, onCleanup, untrack } from "solid-js"
import { Show, createEffect, createMemo, createResource, untrack } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
import { useSearchParams } from "@solidjs/router"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { NewSessionDesignView } from "@/components/session"
import { PromptInput } from "@/components/prompt-input"
import { StatusPopoverV2 } from "@/components/status-popover"
@@ -20,7 +19,6 @@ import { useSync } from "@/context/sync"
import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { useCommand } from "@/context/command"
import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer"
import { useSessionKey } from "@/pages/session/session-layout"
import { useComposerCommands } from "@/pages/session/use-composer-commands"
@@ -43,34 +41,11 @@ export default function NewSessionPage() {
const comments = useComments()
const language = useLanguage()
const settings = useSettings()
const command = useCommand()
const dialog = useDialog()
const route = useSessionKey()
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
useComposerCommands()
useSettingsCommand()
let paletteRun = 0
let paletteDead = false
onCleanup(() => {
paletteDead = true
})
command.register("draft-palette", () => [
{
id: "file.open",
title: language.t("command.file.open"),
description: language.t("palette.search.placeholder"),
category: language.t("command.category.file"),
keybind: "mod+k,mod+p",
onSelect: () => {
const run = ++paletteRun
void import("@/components/dialog-select-file").then((module) => {
if (paletteDead || run !== paletteRun) return
dialog.show(() => <module.DialogSelectFile />)
})
},
},
])
let inputRef: HTMLDivElement | undefined
@@ -8,6 +8,8 @@ test("resets transient prompt input state when the prompt session changes", () =
const [state, setState] = createPromptInputTransientState(identity, 3)
setState({
popover: "slash",
slashMenu: true,
slashMenuQuery: "compact",
historyIndex: 2,
savedPrompt: {
prompt: [{ type: "text", content: "draft-A", start: 0, end: 7 }],
@@ -23,6 +25,8 @@ test("resets transient prompt input state when the prompt session changes", () =
expect(state).toMatchObject({
popover: null,
slashMenu: false,
slashMenuQuery: "",
historyIndex: -1,
savedPrompt: null,
placeholder: 3,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.17.15",
"version": "1.17.14",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.17.15",
"version": "1.17.14",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.17.15",
"version": "1.17.14",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.15",
"version": "1.17.14",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+4 -41
View File
@@ -11,11 +11,6 @@ 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 },
@@ -24,11 +19,6 @@ 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",
@@ -48,7 +38,6 @@ 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) => {
@@ -67,14 +56,11 @@ export const Plugin = define({
)
})
}).pipe(Effect.map((documents) => documents.flat()))
const permissions = expandPermissions(
documents.flatMap((document) => document.info.permissions ?? []),
global.home,
)
const global = documents.flatMap((document) => document.info.permissions ?? [])
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(...permissions))
draft.update(current.id, (agent) => agent.permissions.push(...global))
}
for (const document of documents) {
@@ -87,7 +73,7 @@ export const Plugin = define({
const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...permissions)
if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) {
const model = ModelV2.parse(item.model)
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
@@ -105,9 +91,7 @@ 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(...expandPermissions(item.permissions, global.home))
}
if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
})
}
}
@@ -116,27 +100,6 @@ 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
+4 -11
View File
@@ -22,27 +22,20 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te
- [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"]
## Work State
### Completed
- [finished work, verified facts, or changes made; otherwise "(none)"]
### Active
- [current work, partial changes, or investigation state; otherwise "(none)"]
### Blocked
- [blockers, failing commands, or unknowns; otherwise "(none)"]
- Completed: [finished work, verified facts, or changes made; otherwise "(none)"]
- Active: [current work, partial changes, or investigation state; otherwise "(none)"]
- Blocked: [blockers, failing commands, or unknowns; otherwise "(none)"]
## Next Move
1. [immediate concrete action, or "(none)"]
2. [next action if known, or "(none)"]
## Relevant Files
- [file or directory path: why it matters, or "(none)"]
</template>
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 = {
+1 -76
View File
@@ -8,43 +8,16 @@ 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, Global.node])))
const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.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
@@ -299,51 +272,3 @@ 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
})
}
@@ -1,15 +1,6 @@
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([
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.15",
"version": "1.17.14",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.15",
"version": "1.17.14",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.17.15",
"version": "1.17.14",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.15",
"version": "1.17.14",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.15",
"version": "1.17.14",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
-1
View File
@@ -6,7 +6,6 @@ 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,
-8
View File
@@ -1,8 +0,0 @@
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)
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.15",
"version": "1.17.14",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -1448,7 +1448,6 @@ 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)",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-app",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+1 -6
View File
@@ -218,22 +218,17 @@ 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 session shape.",
"model.efficiencyDescription": "Cost, cache behavior, and average OpenCode Go 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.",
-5
View File
@@ -202,11 +202,6 @@ 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",
-5
View File
@@ -204,11 +204,6 @@ 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",
-5
View File
@@ -203,11 +203,6 @@ 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.",
-5
View File
@@ -204,11 +204,6 @@ 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.",
-5
View File
@@ -203,11 +203,6 @@ 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",
-5
View File
@@ -205,11 +205,6 @@ 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",
-5
View File
@@ -204,11 +204,6 @@ 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",
-5
View File
@@ -205,11 +205,6 @@ 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万あたりのコスト",
-5
View File
@@ -205,11 +205,6 @@ 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만당 비용",
-5
View File
@@ -203,11 +203,6 @@ 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.",
-5
View File
@@ -202,11 +202,6 @@ 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",
-5
View File
@@ -204,11 +204,6 @@ 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 млн",
-5
View File
@@ -204,11 +204,6 @@ 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 ล้าน",
-5
View File
@@ -204,11 +204,6 @@ 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",
-5
View File
@@ -204,11 +204,6 @@ 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 млн",
-5
View File
@@ -203,11 +203,6 @@ 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 万成本",
-5
View File
@@ -203,11 +203,6 @@ 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 萬成本",
+38 -41
View File
@@ -28,6 +28,7 @@ import {
formatCatalogLabName,
getModelCatalog,
type ModelCatalog,
type ModelCatalogCost,
type ModelCatalogEntry,
} from "../model-catalog"
import { SectionHeading } from "../section-heading"
@@ -880,32 +881,35 @@ function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: M
}
>
{(data) => (
<>
<div data-slot="model-efficiency-pattern" aria-hidden="true" />
<div data-component="model-efficiency-grid">
<MetricCard label={i18n.t("model.totalSpendLabel")} value={formatMoney(data().totals.cost)} />
<MetricCard
label={i18n.t("model.costInput")}
value={formatCatalogUnitPrice(props.catalog?.cost?.input)}
/>
<MetricCard
label={i18n.t("model.costOutput")}
value={formatCatalogUnitPrice(props.catalog?.cost?.output)}
/>
<MetricCard
label={i18n.t("model.averageCostSession")}
value={formatSessionCost(data().totals.costPerSession)}
/>
<MetricCard
label={i18n.t("model.averageTokensSession")}
value={formatTokens(data().totals.tokensPerSession)}
/>
<MetricCard
label={i18n.t("model.cacheRatio")}
value={`${formatPercent(data().totals.cacheRatio)} ${i18n.t("model.inputTokens")}`}
/>
</div>
</>
<div data-component="model-metric-grid" data-variant="dense">
<MetricCard
label={i18n.t("model.cost")}
value={formatMoney(data().totals.cost)}
detail={i18n.t("model.totalSpend")}
/>
<MetricCard
label={i18n.t("model.costPerMillion")}
value={
props.catalog?.cost ? formatCatalogPrice(props.catalog.cost) : formatMoney(data().totals.costPerMillion)
}
detail={props.catalog?.cost ? i18n.t("model.inputOutput") : i18n.t("model.observedTokens")}
/>
<MetricCard
label={i18n.t("model.costSession")}
value={formatSessionCost(data().totals.costPerSession)}
detail={i18n.t("model.average")}
/>
<MetricCard
label={i18n.t("model.tokensSession")}
value={formatTokens(data().totals.tokensPerSession)}
detail={i18n.t("model.average")}
/>
<MetricCard
label={i18n.t("model.cacheRatio")}
value={formatPercent(data().totals.cacheRatio)}
detail={i18n.t("model.inputTokens")}
/>
</div>
)}
</Show>
</section>
@@ -1125,12 +1129,12 @@ function ModelPeersSection(props: { data: StatsModelData | null }) {
)
}
function MetricCard(props: { label: string; value: string; detail?: string; state?: "positive" | "negative" }) {
function MetricCard(props: { label: string; value: string; detail: string; state?: "positive" | "negative" }) {
return (
<article data-component="model-metric" data-state={props.state}>
<span>{props.label}</span>
<strong>{props.value}</strong>
<Show when={props.detail}>{(detail) => <p>{detail()}</p>}</Show>
<p>{props.detail}</p>
</article>
)
}
@@ -1143,16 +1147,10 @@ function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) {
href={language.route(`${import.meta.env.BASE_URL}${providerSlug(props.peer.provider)}/${props.peer.slug}`)}
data-active={props.active ? "true" : undefined}
>
<span data-slot="model-peer-rank" aria-label={props.active ? `Rank ${props.peer.rank}` : undefined}>
<Show when={!props.active}>{String(props.peer.rank).padStart(2, "0")}</Show>
</span>
<span data-slot="model-peer-avatar">
<ProviderIcon aria-hidden="true" id={getProviderIconId(props.peer.author)} />
</span>
<span data-slot="model-peer-copy">
<strong>{props.peer.model}</strong>
<em>{props.peer.author}</em>
</span>
<span>{String(props.peer.rank).padStart(2, "0")}</span>
<ProviderIcon aria-hidden="true" id={getProviderIconId(props.peer.author)} />
<strong>{props.peer.model}</strong>
<em>{props.peer.author}</em>
<b>{formatTokens(props.peer.tokens)}</b>
</a>
</li>
@@ -1519,9 +1517,8 @@ function formatMoney(value: number) {
return `$${value.toFixed(value >= 10 ? 0 : 2)}`
}
function formatCatalogUnitPrice(value: number | undefined) {
if (value === undefined) return "-"
return `${formatModelPrice(value)} / 1M`
function formatCatalogPrice(value: ModelCatalogCost) {
return `${formatModelPrice(value.input)} / ${formatModelPrice(value.output)}`
}
function formatModelPrice(value: number) {
+17 -121
View File
@@ -3913,50 +3913,6 @@
color: #b82d35;
}
[data-page="stats"] [data-slot="model-efficiency-pattern"] {
width: 100%;
height: 16px;
margin-bottom: 40px;
overflow: hidden;
background: color-mix(in srgb, var(--stats-text) 10%, transparent);
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
mask-position: center top;
mask-repeat: repeat;
mask-size: 6px 6px;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
-webkit-mask-position: center top;
-webkit-mask-repeat: repeat;
-webkit-mask-size: 6px 6px;
}
[data-page="stats"] [data-component="model-efficiency-grid"] {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] {
display: flex;
align-items: flex-start;
flex-direction: column;
box-sizing: border-box;
gap: 0;
height: 120px;
min-height: 120px;
padding: 16px;
background: var(--stats-layer);
}
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] span {
line-height: 14px;
}
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] strong {
margin-top: 32px;
font-size: 28px;
line-height: 42px;
}
[data-page="stats"] [data-section="model-panel"][data-variant="lab-models"] {
overflow: hidden;
}
@@ -5159,10 +5115,7 @@
[data-page="stats"] [data-component="model-peer-list"] {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(0, 1fr);
grid-template-rows: repeat(5, 64px);
gap: 12px 40px;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
@@ -5170,12 +5123,11 @@
[data-page="stats"] [data-component="model-peer-list"] a {
display: grid;
grid-template-columns: 32px 32px minmax(0, 1fr) minmax(64px, auto);
gap: 16px;
grid-template-columns: 38px 24px minmax(0, 1fr) minmax(96px, auto) minmax(80px, auto);
gap: 12px;
align-items: center;
box-sizing: border-box;
height: 64px;
padding: 15px 16px;
min-height: 44px;
padding: 0 12px;
border: 1px solid var(--stats-line);
background: var(--stats-layer);
color: var(--stats-text);
@@ -5184,56 +5136,25 @@
[data-page="stats"] [data-component="model-peer-list"] a:hover,
[data-page="stats"] [data-component="model-peer-list"] a[data-active="true"] {
border-color: var(--stats-text);
border-color: var(--stats-line-strong);
background: var(--stats-bg);
text-decoration: none;
}
[data-page="stats"] [data-slot="model-peer-rank"],
[data-page="stats"] [data-component="model-peer-list"] span,
[data-page="stats"] [data-component="model-peer-list"] em,
[data-page="stats"] [data-component="model-peer-list"] b {
color: var(--stats-muted);
font-size: 13px;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 16px;
font-variant-numeric: tabular-nums;
line-height: 1.2;
}
[data-page="stats"] [data-slot="model-peer-rank"] {
position: relative;
display: grid;
place-items: center;
width: 32px;
height: 32px;
}
[data-page="stats"] [data-component="model-peer-list"] a[data-active="true"] [data-slot="model-peer-rank"]::before {
width: 6px;
height: 6px;
content: "";
background: var(--stats-text);
}
[data-page="stats"] [data-slot="model-peer-avatar"] {
display: grid;
place-items: center;
width: 32px;
height: 32px;
color: var(--stats-muted);
background: var(--stats-bg);
box-shadow: inset 0 0 0 1px var(--stats-line);
}
[data-page="stats"] [data-slot="model-peer-avatar"] svg {
[data-page="stats"] [data-component="model-peer-list"] svg {
width: 18px;
height: 18px;
}
[data-page="stats"] [data-slot="model-peer-copy"] {
display: grid;
gap: 2px;
min-width: 0;
color: var(--stats-muted);
}
[data-page="stats"] [data-component="model-peer-list"] strong {
@@ -5242,16 +5163,11 @@
color: var(--stats-text);
font-size: 13px;
font-weight: 600;
line-height: 16px;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-page="stats"] [data-component="model-peer-list"] b {
justify-self: end;
color: var(--stats-text);
}
[data-page="stats"][data-theme="dark"],
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) {
color-scheme: dark;
@@ -5783,18 +5699,16 @@
[data-page="stats"] [data-component="model-metric-grid"],
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
[data-page="stats"] [data-component="model-efficiency-grid"],
[data-page="stats"] [data-component="lab-model-grid"] {
grid-template-columns: 1fr 1fr;
}
[data-page="stats"] [data-component="model-peer-list"] a {
grid-template-columns: 32px 32px minmax(0, 1fr) minmax(64px, auto);
gap: 12px;
grid-template-columns: 32px 22px minmax(0, 1fr) minmax(72px, auto);
}
[data-page="stats"] [data-component="model-peer-list"] em {
display: block;
display: none;
}
[data-page="stats"] [data-slot="chart-footer"] {
@@ -6211,7 +6125,6 @@
[data-page="stats"] [data-component="model-metric-grid"],
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
[data-page="stats"] [data-component="model-efficiency-grid"],
[data-page="stats"] [data-component="lab-model-grid"] {
grid-template-columns: 1fr;
}
@@ -6220,15 +6133,6 @@
min-height: 112px;
}
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] {
height: 112px;
min-height: 112px;
}
[data-page="stats"] [data-component="model-efficiency-grid"] [data-component="model-metric"] strong {
margin-top: 24px;
}
[data-page="stats"] [data-component="model-usage-chart"] {
--model-usage-mobile-bar-width: 16px;
--model-usage-mobile-edge-space: 48px;
@@ -6345,20 +6249,12 @@
min-width: 0;
}
[data-page="stats"] [data-component="model-peer-list"] {
grid-auto-flow: row;
grid-auto-columns: auto;
grid-template-rows: none;
gap: 12px;
}
[data-page="stats"] [data-component="model-peer-list"] a {
grid-template-columns: 28px 32px minmax(0, 1fr) minmax(56px, auto);
gap: 12px;
grid-template-columns: 30px minmax(0, 1fr) minmax(64px, auto);
}
[data-page="stats"] [data-slot="model-peer-rank"] {
width: 28px;
[data-page="stats"] [data-component="model-peer-list"] svg {
display: none;
}
[data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -646,8 +646,8 @@ function buildModelTokenMix(aggregate: ModelAggregate): ModelMixEntry[] {
}
function buildModelPeers(peers: ModelAggregate[], rank: number, totalTokens: number): ModelPeerEntry[] {
const start = Math.max(0, Math.min(rank - 5, Math.max(peers.length - 10, 0)))
return peers.slice(start, start + 10).map((item, index) => ({
const start = Math.max(0, Math.min(rank - 4, Math.max(peers.length - 7, 0)))
return peers.slice(start, start + 7).map((item, index) => ({
model: item.model,
provider: item.provider,
author: formatProvider(item.provider),
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-server",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/tui",
"version": "1.17.15",
"version": "1.17.14",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.17.15",
"version": "1.17.14",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.17.15",
"version": "1.17.14",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.17.15",
"version": "1.17.14",
"publisher": "sst-dev",
"repository": {
"type": "git",