mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 19:49:48 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73896b78fc |
@@ -583,7 +583,7 @@ export const dict = {
|
||||
"dialog.usageExceeded.dontShowAgain": "不再显示",
|
||||
|
||||
"context.breakdown.title": "上下文细分",
|
||||
"context.breakdown.note": "输入 token 的大致细分。“其他”包含工具定义和开销。",
|
||||
"context.breakdown.note": "输入词元的大致细分。“其他”包含工具定义和开销。",
|
||||
"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": "总 token",
|
||||
"context.stats.totalTokens": "词元总数",
|
||||
"context.stats.usage": "使用率",
|
||||
"context.stats.inputTokens": "输入 token",
|
||||
"context.stats.outputTokens": "输出 token",
|
||||
"context.stats.reasoningTokens": "推理 token",
|
||||
"context.stats.cacheTokens": "缓存 token(读/写)",
|
||||
"context.stats.inputTokens": "输入词元",
|
||||
"context.stats.outputTokens": "输出词元",
|
||||
"context.stats.reasoningTokens": "推理词元",
|
||||
"context.stats.cacheTokens": "缓存词元(读/写)",
|
||||
"context.stats.userMessages": "用户消息",
|
||||
"context.stats.assistantMessages": "助手消息",
|
||||
"context.stats.totalCost": "总成本",
|
||||
"context.stats.sessionCreated": "创建时间",
|
||||
"context.stats.lastActivity": "最后活动",
|
||||
"context.usage.tokens": "Token",
|
||||
"context.usage.tokens": "词元数",
|
||||
"context.usage.usage": "使用率",
|
||||
"context.usage.cost": "成本",
|
||||
"context.usage.clickToView": "点击查看上下文",
|
||||
|
||||
@@ -676,9 +676,16 @@ export default function Page() {
|
||||
const mode = reviewMode()
|
||||
if (mode === "git" || mode === "branch") return mode
|
||||
})
|
||||
const vcsWorkspace = createMemo(() => info()?.workspaceID)
|
||||
const vcsKey = createMemo(
|
||||
() =>
|
||||
["session-vcs", sdk().directory, sync().data.vcs?.branch ?? "", sync().data.vcs?.default_branch ?? ""] as const,
|
||||
[
|
||||
"session-vcs",
|
||||
sdk().directory,
|
||||
vcsWorkspace() ?? "",
|
||||
sync().data.vcs?.branch ?? "",
|
||||
sync().data.vcs?.default_branch ?? "",
|
||||
] as const,
|
||||
)
|
||||
const vcsQuery = createQuery(() => {
|
||||
const mode = vcsMode()
|
||||
@@ -690,7 +697,10 @@ export default function Page() {
|
||||
queryFn: mode
|
||||
? () =>
|
||||
sdk()
|
||||
.api.vcs.diff({ location: { directory: sdk().directory }, mode: mode === "git" ? "working" : mode })
|
||||
.api.vcs.diff({
|
||||
location: { directory: sdk().directory, workspace: vcsWorkspace() },
|
||||
mode: mode === "git" ? "working" : mode,
|
||||
})
|
||||
.then((result) => result.data)
|
||||
.catch((error) => {
|
||||
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
||||
@@ -739,7 +749,7 @@ export default function Page() {
|
||||
queryFn: () =>
|
||||
sdk()
|
||||
.api.vcs.diff({
|
||||
location: { directory: scope },
|
||||
location: { directory: scope, workspace: vcsWorkspace() },
|
||||
mode: mode === "git" ? "working" : mode,
|
||||
context,
|
||||
})
|
||||
|
||||
@@ -184,6 +184,20 @@ describe("createCompatibleApi", () => {
|
||||
expect(url.searchParams.get("limit")).toBe("20")
|
||||
})
|
||||
|
||||
test("routes V1 diffs through the requested workspace", async () => {
|
||||
const { api, requests } = setup("v1")
|
||||
await api.vcs.diff({
|
||||
location: { directory: "/repo", workspace: "workspace-1" },
|
||||
mode: "working",
|
||||
})
|
||||
|
||||
const url = new URL(requests[0]!.url)
|
||||
expect(url.pathname).toBe("/vcs/diff")
|
||||
expect(url.searchParams.get("directory")).toBe("/repo")
|
||||
expect(url.searchParams.get("workspace")).toBe("workspace-1")
|
||||
expect(url.searchParams.get("mode")).toBe("git")
|
||||
})
|
||||
|
||||
test("routes V1 permission replies through the requested directory", async () => {
|
||||
const { api, requests } = setup("v1")
|
||||
await api.permission.reply({
|
||||
|
||||
@@ -344,6 +344,7 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
|
||||
const result = await legacy(value.location).vcs.diff({
|
||||
mode: value.mode === "working" ? "git" : value.mode,
|
||||
context: value.context,
|
||||
workspace: value.location?.workspace,
|
||||
})
|
||||
return located(
|
||||
(result.data ?? []).map((file) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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.
|
||||
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.
|
||||
|
||||
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 {{MODEL_NAME}}.
|
||||
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta Muse Spark.
|
||||
- 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.
|
||||
|
||||
@@ -25,10 +25,7 @@ import { MCP } from "@/mcp"
|
||||
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
||||
|
||||
export function provider(model: Provider.Model) {
|
||||
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("muse-spark")) return [PROMPT_META]
|
||||
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")) {
|
||||
|
||||
@@ -85,21 +85,9 @@ const it = testEffect(
|
||||
|
||||
describe("session.system", () => {
|
||||
test("selects the Meta prompt for Muse Spark model IDs", () => {
|
||||
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}}")
|
||||
}
|
||||
expect(SystemPrompt.provider({ api: { id: "meta/muse-spark-preview" } } as Provider.Model)[0]).toContain(
|
||||
"Meta Muse Spark",
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("skills output is sorted by name and stable across calls", () =>
|
||||
|
||||
@@ -517,15 +517,7 @@ export type TuiSlots = {
|
||||
}
|
||||
|
||||
export type TuiEventBus = {
|
||||
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
|
||||
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => () => void
|
||||
}
|
||||
|
||||
export type TuiDispose = () => void | Promise<void>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import type { Event } from "@opencode-ai/sdk/v2"
|
||||
import type { TuiAttentionSoundName, TuiEventMetadata, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||
import type { TuiAttentionSoundName, 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,
|
||||
@@ -38,41 +32,32 @@ const tui: TuiPlugin = async (api) => {
|
||||
const questions = new Set<string>()
|
||||
const permissions = new Set<string>()
|
||||
|
||||
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
|
||||
api.event.on("question.asked", (event) => {
|
||||
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, metadata) => {
|
||||
if (!matchesSession(api, event.properties.sessionID, metadata)) return
|
||||
api.event.on("question.replied", (event) => {
|
||||
questions.delete(event.properties.requestID)
|
||||
})
|
||||
|
||||
api.event.on("question.rejected", (event, metadata) => {
|
||||
if (!matchesSession(api, event.properties.sessionID, metadata)) return
|
||||
api.event.on("question.rejected", (event) => {
|
||||
questions.delete(event.properties.requestID)
|
||||
})
|
||||
|
||||
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
|
||||
api.event.on("permission.asked", (event) => {
|
||||
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, metadata) => {
|
||||
if (!matchesSession(api, event.properties.sessionID, metadata)) return
|
||||
api.event.on("permission.replied", (event) => {
|
||||
permissions.delete(event.properties.requestID)
|
||||
})
|
||||
|
||||
api.event.on("session.status", (event, metadata) => {
|
||||
api.event.on("session.status", (event) => {
|
||||
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)
|
||||
@@ -92,10 +77,9 @@ const tui: TuiPlugin = async (api) => {
|
||||
notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done")
|
||||
})
|
||||
|
||||
api.event.on("session.error", (event, metadata) => {
|
||||
api.event.on("session.error", (event) => {
|
||||
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,14 +4,9 @@ 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(input: { auto?: boolean } = {}) {
|
||||
async function setup() {
|
||||
const notifications: TuiAttentionNotifyInput[] = []
|
||||
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 handlers = new Map<Event["type"], ((event: Event) => void)[]>()
|
||||
const session = (id: string, title: string, parentID?: string): Session => ({
|
||||
id,
|
||||
title,
|
||||
@@ -38,18 +33,9 @@ async function setup(input: { auto?: boolean } = {}) {
|
||||
},
|
||||
},
|
||||
event: {
|
||||
on: <Type extends Event["type"]>(
|
||||
type: Type,
|
||||
handler: (
|
||||
event: Extract<Event, { type: Type }>,
|
||||
metadata: { directory: string; workspace: string | undefined },
|
||||
) => void,
|
||||
) => {
|
||||
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => {
|
||||
const list = handlers.get(type) ?? []
|
||||
const wrapped = handler as (
|
||||
event: Event,
|
||||
metadata: { directory: string; workspace: string | undefined },
|
||||
) => void
|
||||
const wrapped = handler as (event: Event) => void
|
||||
list.push(wrapped)
|
||||
handlers.set(type, list)
|
||||
return () => {
|
||||
@@ -63,8 +49,6 @@ async function setup(input: { auto?: boolean } = {}) {
|
||||
state: {
|
||||
session: {
|
||||
get: (sessionID: string) => sessions[sessionID],
|
||||
permission: (sessionID: string) => permissions[sessionID] ?? [],
|
||||
question: (sessionID: string) => questions[sessionID] ?? [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -74,30 +58,8 @@ async function setup(input: { auto?: boolean } = {}) {
|
||||
|
||||
return {
|
||||
notifications,
|
||||
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)
|
||||
emit(event: Event) {
|
||||
for (const handler of handlers.get(event.type) ?? []) handler(event)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -174,52 +136,6 @@ 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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user