Build OpenCode plugins with TUI integration using Toast notifications, session prompts, and prompt manipulation. Covers client.tui.showToast(), client.session.prompt() with noReply, and client.tui.appendPrompt() APIs with TypeScript examples and best practices.
Build rich user experiences in OpenCode plugins using TUI (Terminal User Interface) APIs.
Show non-intrusive feedback with client.tui.showToast():
import { Plugin, tool } from "@opencode-ai/plugin"
export const MyPlugin: Plugin = async ({ client }) => {
return {
tool: {
processData: tool({
description: "Process with status notifications",
args: { input: tool.schema.string() },
async execute({ input }) {
// Show loading toast
await client.tui.showToast({
body: { message: "Processing...", variant: "loading" }
})
const result = await processData(input)
// Show success toast
await client.tui.showToast({
body: { message: "Complete!", variant: "success" }
})
return result
}
})
}
}
}
Toast Variants:
"default" - General information (neutral)"success" - Operation completed (green)"error" - Error occurred (red)"loading" - Operation in progress (spinner)Error Handling:
async execute({ input }) {
try {
const result = await riskyOperation(input)
await client.tui.showToast({
body: { message: "Success!", variant: "success" }
})
return result
} catch (error) {
await client.tui.showToast({
body: { message: `Error: ${error.message}`, variant: "error" }
})
throw error
}
}
Send messages to users without triggering AI responses using noReply: true:
export const MyPlugin: Plugin = async ({ client }) => {
return {
tool: {
notifyUser: tool({
description: "Send notification to user",
args: { message: tool.schema.string() },
async execute({ message }, context) {
const { sessionID } = context
await client.session.prompt({
path: { id: sessionID },
body: {
noReply: true, // Critical: prevents AI response
parts: [{ type: "text", text: message }]
}
})
return "Message sent"
}
})
}
}
}
Tool Context:
sessionID - Target session identifiermessageID - Current message identifieragent - Current agent nameabort - Abort signalInsert text into user's prompt input:
async execute({ suggestion }) {
await client.tui.appendPrompt({
body: { text: suggestion }
})
return "Suggestion added"
}
// Correct - context-only message
await client.session.prompt({
path: { id: sessionID },
body: {
noReply: true,
parts: [{ type: "text", text: "Progress update..." }]
}
})
// Incorrect - triggers AI response
await client.session.prompt({
path: { id: sessionID },
body: {
parts: [{ type: "text", text: "Progress update..." }]
}
})
async execute(args, context) {
const { sessionID } = context // Use this
// ...
}
async execute(args, context) {
try {
await client.tui.showToast({
body: { message: "Working...", variant: "loading" }
})
// Do work...
} catch (error) {
try {
await client.tui.showToast({
body: { message: `Error: ${error.message}`, variant: "error" }
})
} catch (toastError) {
console.error("Failed to show error toast:", toastError)
}
throw error
}
}
// Too many - avoid this
await client.tui.showToast({ body: { message: "Step 1 done", variant: "success" }})
await client.tui.showToast({ body: { message: "Step 2 done", variant: "success" }})
// Better - single summary
await client.tui.showToast({
body: { message: "All 3 steps completed", variant: "success" }
})
import { Plugin, tool } from "@opencode-ai/plugin"
export const UIIntegrationPlugin: Plugin = async ({ client }) => {
return {
tool: {
// Toast notifications
showStatus: tool({
description: "Show status toast",
args: {
message: tool.schema.string(),
type: tool.schema.enum(["info", "success", "error", "loading"]).default("info")
},
async execute({ message, type }) {
await client.tui.showToast({
body: { message, variant: type }
})
return `Toast shown: ${message}`
}
}),
// User messaging
sendMessage: tool({
description: "Send message to user",
args: { text: tool.schema.string() },
async execute({ text }, context) {
const { sessionID } = context
await client.session.prompt({
path: { id: sessionID },
body: {
noReply: true,
parts: [{ type: "text", text }]
}
})
return "Message delivered"
}
}),
// Prompt suggestions
suggestInput: tool({
description: "Suggest text to add to user's prompt",
args: { suggestion: tool.schema.string() },
async execute({ suggestion }) {
await client.tui.appendPrompt({
body: { text: suggestion }
})
return "Suggestion added"
}
}),
// Combined workflow
processWithFeedback: tool({
description: "Process with full UI feedback",
args: { data: tool.schema.string() },
async execute({ data }, context) {
const { sessionID } = context
// Start notification
await client.tui.showToast({
body: { message: "Starting...", variant: "loading" }
})
await client.session.prompt({
path: { id: sessionID },
body: {
noReply: true,
parts: [{ type: "text", text: `Processing: ${data}` }]
}
})
// Do work
await new Promise(resolve => setTimeout(resolve, 2000))
// Success notification
await client.tui.showToast({
body: { message: "Complete!", variant: "success" }
})
return "Workflow complete"
}
})
}
}
}
Parameters:
body.message (string): Message to displaybody.variant (string): "default", "success", "error", "loading"Returns: boolean
Parameters:
body.text (string): Text to append to promptReturns: boolean
Parameters:
path.id (string): Session IDbody.noReply (boolean): true for context-onlybody.parts (array): Message parts [{ type: "text", text: "..." }]body.model (optional): Override modelReturns: AssistantMessage | UserMessage
client.tui.openHelp() - Open help dialogclient.tui.openSessions() - Open session selectorclient.tui.openThemes() - Open theme selectorclient.tui.openModels() - Open model selectorclient.tui.submitPrompt() - Submit current promptclient.tui.clearPrompt() - Clear prompt inputclient.tui.executeCommand({ body: { command: "..." } }) - Execute TUI commandUse this skill when:
@opencode-ai/plugin package