diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index f76d1aaf9d22..27243b15e711 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -82,6 +82,7 @@ export const Flag = { OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"), OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"), + OPENCODE_DISABLE_NOTIFICATIONS: truthy("OPENCODE_DISABLE_NOTIFICATIONS"), OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"), OPENCODE_EXPERIMENTAL_EVENT_SYSTEM: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"), diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 7011b51eb90c..9a69085aeb53 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -21,6 +21,7 @@ import { Flag } from "@opencode-ai/core/flag/flag" import { ServerAuth } from "@/server/auth" import { EOL } from "os" import { Filesystem } from "@/util/filesystem" +import { Notify } from "@/cli/notify" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { Agent } from "@/agent/agent" import { Permission } from "@/permission" @@ -601,6 +602,9 @@ export const RunCommand = effectCmd({ async function loop(client: OpencodeClient, events: Awaited>) { const toggles = new Map() let error: string | undefined + let lastOutput = "" + let agent = "" + let model = "" for await (const event of events.stream) { if ( @@ -610,8 +614,10 @@ export const RunCommand = effectCmd({ args.format !== "json" && toggles.get("start") !== true ) { + agent = event.properties.info.agent ?? "" + model = event.properties.info.modelID ?? "" UI.empty() - UI.println(`> ${event.properties.info.agent} · ${event.properties.info.modelID}`) + UI.println(`> ${agent} · ${model}`) UI.empty() toggles.set("start", true) } @@ -653,6 +659,7 @@ export const RunCommand = effectCmd({ if (emit("text", { part })) continue const text = part.text.trim() if (!text) continue + lastOutput = text if (!process.stdout.isTTY) { process.stdout.write(text + EOL) continue @@ -694,6 +701,18 @@ export const RunCommand = effectCmd({ event.properties.sessionID === sessionID && event.properties.status.type === "idle" ) { + const prompt = message.slice(0, 100) + if (error) { + Notify.notify(prompt, `✗ ${error.slice(0, 200)}`) + } else if (lastOutput) { + const lines = lastOutput.split("\n").filter(Boolean).slice(0, 3) + const via = agent || model ? `${agent}${model ? ` (${model})` : ""}\n` : "" + const snippet = lines.join("\n").slice(0, 200) + Notify.notify(prompt, `${via}${snippet}`) + } else { + const via = agent || model ? `${agent}${model ? ` (${model})` : ""}` : "" + Notify.notify(prompt, via || "✓ done") + } break } diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index d7f2cd14b0ee..648357148cb3 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -61,6 +61,7 @@ import { PromptRefProvider, usePromptRef } from "./context/prompt" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" +import { Notify } from "@/cli/notify" import { createTuiApi } from "@/cli/cmd/tui/plugin/api" import type { RouteMap } from "@/cli/cmd/tui/plugin/api" import { FormatError, FormatUnknownError } from "@/cli/error" @@ -869,11 +870,18 @@ function App(props: { onSnapshot?: () => Promise }) { } }) + event.on("session.idle", (evt) => { + const session = sync.session.get(evt.properties.sessionID) + Notify.notify("OpenCode", session?.title ?? "Task complete") + }) + event.on("session.error", (evt) => { const error = evt.properties.error if (error && typeof error === "object" && error.name === "MessageAbortedError") return const message = errorMessage(error) + Notify.notifyError("OpenCode Error", message) + toast.show({ variant: "error", message, diff --git a/packages/opencode/src/cli/notify.ts b/packages/opencode/src/cli/notify.ts new file mode 100644 index 000000000000..8f88d580a0d0 --- /dev/null +++ b/packages/opencode/src/cli/notify.ts @@ -0,0 +1,20 @@ +import * as Process from "@/util/process" +import { Flag } from "@opencode-ai/core/flag/flag" + +const noop = () => {} + +export const notify = Flag.OPENCODE_DISABLE_NOTIFICATIONS + ? noop + : (title: string, message?: string) => { + const { platform } = process + if (platform === "linux") { + Process.spawn(["notify-send", title, message ?? ""]) + } else if (platform === "darwin") { + const msg = (message ?? "").replace(/"/g, '\\"') + Process.spawn(["osascript", "-e", `display notification "${msg}" with title "${title}"`]) + } + } + +export const notifyError = notify + +export * as Notify from "./notify"