Compare commits

...

3 Commits

Author SHA1 Message Date
Sebastian Herrlinger 97c49512ac fix(tui): scope attention notifications 2026-08-10 20:21:46 +00:00
Matthias Reso b9f3b382fc fix(session): route all Muse family models to the Meta system prompt (#41581) 2026-08-10 13:28:04 -05:00
opencode-agent[bot] 550d1ffd24 fix(i18n): use widely recognized developer terminology (#41532)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-10 22:00:00 +10:00
7 changed files with 151 additions and 28 deletions
+7 -7
View File
@@ -583,7 +583,7 @@ export const dict = {
"dialog.usageExceeded.dontShowAgain": "不再显示",
"context.breakdown.title": "上下文细分",
"context.breakdown.note": "输入词元的大致细分。“其他”包含工具定义和开销。",
"context.breakdown.note": "输入 token 的大致细分。“其他”包含工具定义和开销。",
"context.breakdown.system": "系统",
"context.breakdown.user": "用户",
"context.breakdown.assistant": "助手",
@@ -598,18 +598,18 @@ export const dict = {
"context.stats.provider": "提供商",
"context.stats.model": "模型",
"context.stats.limit": "上下文限制",
"context.stats.totalTokens": "词元总数",
"context.stats.totalTokens": "总 token",
"context.stats.usage": "使用率",
"context.stats.inputTokens": "输入词元",
"context.stats.outputTokens": "输出词元",
"context.stats.reasoningTokens": "推理词元",
"context.stats.cacheTokens": "缓存词元(读/写)",
"context.stats.inputTokens": "输入 token",
"context.stats.outputTokens": "输出 token",
"context.stats.reasoningTokens": "推理 token",
"context.stats.cacheTokens": "缓存 token(读/写)",
"context.stats.userMessages": "用户消息",
"context.stats.assistantMessages": "助手消息",
"context.stats.totalCost": "总成本",
"context.stats.sessionCreated": "创建时间",
"context.stats.lastActivity": "最后活动",
"context.usage.tokens": "词元数",
"context.usage.tokens": "Token",
"context.usage.usage": "使用率",
"context.usage.cost": "成本",
"context.usage.clickToView": "点击查看上下文",
@@ -1,4 +1,4 @@
You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by Muse Spark, a large language model trained by Meta MSL.
You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by {{MODEL_NAME}}, a large language model trained by Meta MSL.
Use the instructions below and the tools available to assist the user.
@@ -61,5 +61,5 @@ Use the instructions below and the tools available to assist the user.
- NEVER use comments as a place for long-winded chain-of-thought. Long thinking texts must be generated as private reasoning. Comments in code must be appropriately concise.
# User Help & Feedback
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta Muse Spark.
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta {{MODEL_NAME}}.
- When users ask directly about OpenCode (eg. "can OpenCode do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from the OpenCode docs at https://opencode.ai/docs.
+4 -1
View File
@@ -25,7 +25,10 @@ import { MCP } from "@/mcp"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
export function provider(model: Provider.Model) {
if (model.api.id.includes("muse-spark")) return [PROMPT_META]
if (model.api.id.includes("muse")) {
const name = model.api.id.includes("muse-glimmer") ? "Muse Glimmer" : "Muse Spark"
return [PROMPT_META.replaceAll("{{MODEL_NAME}}", name)]
}
if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3"))
return [PROMPT_BEAST]
if (model.api.id.includes("gpt")) {
+15 -3
View File
@@ -85,9 +85,21 @@ const it = testEffect(
describe("session.system", () => {
test("selects the Meta prompt for Muse Spark model IDs", () => {
expect(SystemPrompt.provider({ api: { id: "meta/muse-spark-preview" } } as Provider.Model)[0]).toContain(
"Meta Muse Spark",
)
for (const id of ["meta/muse-spark-preview", "muse-spark-1.1", "muse-spark-1.2"]) {
const prompt = SystemPrompt.provider({ api: { id } } as Provider.Model)[0]
expect(prompt).toContain("powered by Muse Spark,")
expect(prompt).toContain("using Meta Muse Spark.")
expect(prompt).not.toContain("{{MODEL_NAME}}")
}
})
test("selects the Meta prompt for Muse Glimmer model IDs", () => {
for (const id of ["meta/muse-glimmer", "meta/muse-glimmer-30b", "muse-glimmer-30b"]) {
const prompt = SystemPrompt.provider({ api: { id } } as Provider.Model)[0]
expect(prompt).toContain("powered by Muse Glimmer,")
expect(prompt).toContain("using Meta Muse Glimmer.")
expect(prompt).not.toContain("{{MODEL_NAME}}")
}
})
it.effect("skills output is sorted by name and stable across calls", () =>
+9 -1
View File
@@ -517,7 +517,15 @@ export type TuiSlots = {
}
export type TuiEventBus = {
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => () => void
on: <Type extends Event["type"]>(
type: Type,
handler: (event: Extract<Event, { type: Type }>, metadata: TuiEventMetadata) => void,
) => () => void
}
export type TuiEventMetadata = {
directory: string
workspace: string | undefined
}
export type TuiDispose = () => void | Promise<void>
@@ -1,13 +1,19 @@
import type { Event } from "@opencode-ai/sdk/v2"
import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
import type { TuiAttentionSoundName, TuiEventMetadata, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
import type { BuiltinTuiPlugin } from "../builtins"
const id = "internal:notifications"
type SessionError = Extract<Event, { type: "session.error" }>["properties"]["error"]
function matchesSession(api: TuiPluginApi, sessionID: string, metadata: TuiEventMetadata) {
const session = api.state.session.get(sessionID)
return session?.directory === metadata.directory && session.workspaceID === metadata.workspace
}
function notify(api: TuiPluginApi, sessionID: string | undefined, message: string, sound: TuiAttentionSoundName) {
const session = sessionID ? api.state.session.get(sessionID) : undefined
if (!session) return
const isSubagent = session?.parentID !== undefined
void api.attention.notify({
title: session?.title,
@@ -32,32 +38,41 @@ const tui: TuiPlugin = async (api) => {
const questions = new Set<string>()
const permissions = new Set<string>()
api.event.on("question.asked", (event) => {
api.event.on("question.asked", (event, metadata) => {
if (!matchesSession(api, event.properties.sessionID, metadata)) return
if (!api.state.session.question(event.properties.sessionID).some((item) => item.id === event.properties.id)) return
if (questions.has(event.properties.id)) return
questions.add(event.properties.id)
notify(api, event.properties.sessionID, "Question needs input", "question")
})
api.event.on("question.replied", (event) => {
api.event.on("question.replied", (event, metadata) => {
if (!matchesSession(api, event.properties.sessionID, metadata)) return
questions.delete(event.properties.requestID)
})
api.event.on("question.rejected", (event) => {
api.event.on("question.rejected", (event, metadata) => {
if (!matchesSession(api, event.properties.sessionID, metadata)) return
questions.delete(event.properties.requestID)
})
api.event.on("permission.asked", (event) => {
api.event.on("permission.asked", (event, metadata) => {
if (!matchesSession(api, event.properties.sessionID, metadata)) return
if (!api.state.session.permission(event.properties.sessionID).some((item) => item.id === event.properties.id))
return
if (permissions.has(event.properties.id)) return
permissions.add(event.properties.id)
notify(api, event.properties.sessionID, "Permission needs input", "permission")
})
api.event.on("permission.replied", (event) => {
api.event.on("permission.replied", (event, metadata) => {
if (!matchesSession(api, event.properties.sessionID, metadata)) return
permissions.delete(event.properties.requestID)
})
api.event.on("session.status", (event) => {
api.event.on("session.status", (event, metadata) => {
const sessionID = event.properties.sessionID
if (!matchesSession(api, sessionID, metadata)) return
if (event.properties.status.type === "busy" || event.properties.status.type === "retry") {
active.add(sessionID)
errored.delete(sessionID)
@@ -77,9 +92,10 @@ const tui: TuiPlugin = async (api) => {
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
})
api.event.on("session.error", (event) => {
api.event.on("session.error", (event, metadata) => {
const sessionID = event.properties.sessionID
if (!sessionID) return
if (!matchesSession(api, sessionID, metadata)) return
if (!active.has(sessionID)) return
errored.add(sessionID)
notify(api, sessionID, sessionErrorMessage(event.properties.error), "error")
@@ -4,9 +4,14 @@ import type { Event, PermissionRequest, QuestionRequest, Session } from "@openco
import type { TuiAttentionNotifyInput } from "@opencode-ai/plugin/tui"
import { createTuiPluginApi } from "../../../fixture/tui-plugin"
async function setup() {
async function setup(input: { auto?: boolean } = {}) {
const notifications: TuiAttentionNotifyInput[] = []
const handlers = new Map<Event["type"], ((event: Event) => void)[]>()
const handlers = new Map<
Event["type"],
((event: Event, metadata: { directory: string; workspace: string | undefined }) => void)[]
>()
const permissions: Record<string, PermissionRequest[]> = {}
const questions: Record<string, QuestionRequest[]> = {}
const session = (id: string, title: string, parentID?: string): Session => ({
id,
title,
@@ -33,9 +38,18 @@ async function setup() {
},
},
event: {
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => {
on: <Type extends Event["type"]>(
type: Type,
handler: (
event: Extract<Event, { type: Type }>,
metadata: { directory: string; workspace: string | undefined },
) => void,
) => {
const list = handlers.get(type) ?? []
const wrapped = handler as (event: Event) => void
const wrapped = handler as (
event: Event,
metadata: { directory: string; workspace: string | undefined },
) => void
list.push(wrapped)
handlers.set(type, list)
return () => {
@@ -49,6 +63,8 @@ async function setup() {
state: {
session: {
get: (sessionID: string) => sessions[sessionID],
permission: (sessionID: string) => permissions[sessionID] ?? [],
question: (sessionID: string) => questions[sessionID] ?? [],
},
},
}),
@@ -58,8 +74,30 @@ async function setup() {
return {
notifications,
emit(event: Event) {
for (const handler of handlers.get(event.type) ?? []) handler(event)
emit(event: Event, metadata = { directory: "/workspace", workspace: undefined as string | undefined }) {
if (event.type === "permission.asked" && !input.auto) {
permissions[event.properties.sessionID] = [
...(permissions[event.properties.sessionID] ?? []).filter((item) => item.id !== event.properties.id),
event.properties,
]
}
if (event.type === "permission.replied") {
permissions[event.properties.sessionID] = (permissions[event.properties.sessionID] ?? []).filter(
(item) => item.id !== event.properties.requestID,
)
}
if (event.type === "question.asked") {
questions[event.properties.sessionID] = [
...(questions[event.properties.sessionID] ?? []).filter((item) => item.id !== event.properties.id),
event.properties,
]
}
if (event.type === "question.replied" || event.type === "question.rejected") {
questions[event.properties.sessionID] = (questions[event.properties.sessionID] ?? []).filter(
(item) => item.id !== event.properties.requestID,
)
}
for (const handler of handlers.get(event.type) ?? []) handler(event, metadata)
},
}
}
@@ -136,6 +174,52 @@ describe("internal notifications TUI plugin", () => {
])
})
test("suppresses auto-approved permission requests", async () => {
const harness = await setup({ auto: true })
harness.emit({ id: "event-1", type: "permission.asked", properties: permission("permission-1") })
expect(harness.notifications).toEqual([])
})
test("ignores events outside the session location", async () => {
const harness = await setup()
const foreign = { directory: "/other", workspace: undefined }
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1") }, foreign)
harness.emit({ id: "event-2", type: "permission.asked", properties: permission("permission-1") }, foreign)
harness.emit(
{ id: "event-2b", type: "permission.asked", properties: permission("permission-2") },
{ directory: "/workspace", workspace: "other-workspace" },
)
harness.emit(
{
id: "event-3",
type: "session.status",
properties: { sessionID: "session", status: { type: "busy" } },
},
foreign,
)
harness.emit(
{
id: "event-4",
type: "session.error",
properties: { sessionID: "session", error: { name: "UnknownError", data: { message: "boom" } } },
},
foreign,
)
harness.emit(
{
id: "event-5",
type: "session.status",
properties: { sessionID: "session", status: { type: "idle" } },
},
foreign,
)
expect(harness.notifications).toEqual([])
})
test("notifies when an active session becomes idle and suppresses no-op idle", async () => {
const harness = await setup()