7.1 KiB
Commands And Feedback
This document is the normative SDK contract for plugin commands that may launch background work.
Contract
Every menu click or plugin slash command follows this sequence:
manifest contribution -> command id -> registered command handler -> optional task -> feedback surfaces
- A manifest menu item declares a
commandid; it does not run tools directly. - A manifest slash command declares a slash
name, autocomplete metadata and acommandid; it does not run tools directly. - Menu items and slash commands may share the same
commandid, or point to different handlers. The plugin owns that choice. - The command handler is the only place that decides whether work should start.
- The host slash-command registry only lists/filters metadata and returns a callback dispatch effect. The plugin handler decides what the command does.
- A launched process is represented by a background task returned from
ctx.services.tasks.runCommand(). - A skipped command is represented by the command handler return value and logs, not by a fake task.
- Feedback objects must be stable enough for agents and programmatic callers to parse, and their messages must be readable by humans.
- The current human menu-click UI does not guarantee display of a command
handler return value. When a human must see feedback from a menu action, open
or focus a plugin layout/window from the handler with
ctx.services.windows.open({ layoutType, state }).
Preconditions
Check preconditions before calling runCommand():
ctx.servicesexists. Plugins need theuiortoolingcapability for the service facade.ctx.services.workspace.getCurrentProject()returned a project, or the caller supplied a validprojectId.- Required executables, environment variables and workspace files were validated,
preferably with
ctx.services.tooling.diagnose(). ownerAgentIdis a real agent id when the work is owned by an agent workflow. Do not use all-zero placeholders in production.cwdis relative to the project root.command,argsandcwddo not resolve package-relative plugin resources. Do not userunCommand()to launch scripts shipped inside the plugin package; there is no public runtimepluginRootpath inactivate(ctx).commandandargsare separate values. Do not shell-join user input.
If any required precondition fails, return a skipped result:
return {
status: "skipped",
reason: "missing-owner-agent",
message: "Configure an owner agent before launching the hello-plugin task."
};
Feedback Surfaces
Command handlers should return one of these shapes, or a plugin-specific object with equivalent fields:
type CommandFeedback =
| { status: "skipped"; reason: string; message: string }
| { status: "launched"; taskId: string; state: string; message: string };
Use the same status vocabulary consistently:
skipped: no background task was created.launched: a task was created; inspect the task state for later progress.failed: the handler itself failed before it could return normally.
Visible surfaces are intentionally distinct:
- Command return value: immediate feedback for programmatic callers, agents and future host surfaces. It is not a guaranteed visible UI surface for current human menu clicks.
- Plugin layout/window: the normative visible feedback surface for menu actions
that need to show status, results or next steps to a human. The layout must be
declared in
idea-plugin.jsonand registered during activation. - Plugin logs: diagnostics for developers and operators.
- Work/background-task surfaces: only for tasks actually launched through
runCommand(). - Plugin layouts or files: optional plugin-owned user feedback.
recordOnly: true affects completion delivery to the owning agent. It does not
mean hidden, skipped or UI-silent. A record-only command task can still appear in
Work and can still emit background-task events.
Best-Effort Watches
ctx.services.workspace.watch() is not a baseline precondition for commands.
The host may reject it until workspace watch support is delivered. Treat watch
setup as optional and non-fatal:
try {
const watch = await ctx.services.workspace.watch(".ideai", refresh, project.id);
ctx.subscriptions.push(watch);
} catch (error) {
ctx.logger.info("workspace watch unavailable", { error });
}
Commands that depend on fresh workspace state should refresh or re-read that state when invoked instead of assuming a watch was installed at activation.
Example
Visible menu feedback:
import type { IdeAPluginModule, PluginLayoutProps } from "@idea/plugin-sdk";
function HealthView(_props: PluginLayoutProps<{ source?: string }>) {
return <section>Unity tools are ready.</section>;
}
const plugin: IdeAPluginModule = {
activate(ctx) {
ctx.layouts?.register({
type: "unity-plugin.health",
component: HealthView
});
ctx.commands?.registerCommand("unity-plugin.health", async () => {
const win = await ctx.services?.windows.open({
layoutType: "unity-plugin.health",
state: { source: "menu" }
});
return win
? { status: "opened", message: "Opened Unity Health.", alreadyOpen: win.alreadyOpen }
: { status: "skipped", reason: "ui-service-unavailable", message: "UI service unavailable." };
});
}
};
export default plugin;
Background task feedback:
const project = await ctx.services?.workspace.getCurrentProject();
if (!project) {
return {
status: "skipped",
reason: "no-focused-project",
message: "Open a project before running this command."
};
}
const ownerAgentId = await ctx.storage?.get<string>("myPlugin.ownerAgentId");
if (!ownerAgentId) {
return {
status: "skipped",
reason: "missing-owner-agent",
message: "Configure an owner agent before launching this task."
};
}
const task = await ctx.services.tasks.runCommand({
projectId: project.id,
ownerAgentId,
label: "Run my tool",
command: "npm",
args: ["--version"],
cwd: ".",
recordOnly: true
});
return {
status: "launched",
taskId: task.taskId,
state: task.state,
message: "Started Run my tool."
};
Counterexample
Do not launch a shell command just to produce feedback:
await ctx.services?.tasks.runCommand({
ownerAgentId: "00000000-0000-0000-0000-000000000000",
label: "Skipped: missing config",
command: "echo",
args: ["missing config"],
recordOnly: true
});
This creates misleading Work history, uses a placeholder owner and turns a
precondition failure into a fake task. Return status: "skipped" instead.
Do not try to reach packaged plugin scripts through workspace-relative paths:
await ctx.services?.tasks.runCommand({
projectId: project.id,
ownerAgentId,
label: "Run packaged tests",
command: "scripts/run-unity-tests.sh",
cwd: "."
});
scripts/run-unity-tests.sh above is resolved like any other executable visible
from the project task environment; it is not resolved relative to the plugin
package. Use a project-owned script or an executable available on PATH until a
dedicated packaged-resource API exists.