Files
IdeaSDK/docs/commands-and-feedback.md

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 command id; it does not run tools directly.
  • A manifest slash command declares a slash name, autocomplete metadata and a command id; it does not run tools directly.
  • Menu items and slash commands may share the same command id, 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.services exists. Plugins need the ui or tooling capability for the service facade.
  • ctx.services.workspace.getCurrentProject() returned a project, or the caller supplied a valid projectId.
  • Required executables, environment variables and workspace files were validated, preferably with ctx.services.tooling.diagnose().
  • ownerAgentId is a real agent id when the work is owned by an agent workflow. Do not use all-zero placeholders in production.
  • cwd is relative to the project root.
  • command, args and cwd do not resolve package-relative plugin resources. Do not use runCommand() to launch scripts shipped inside the plugin package; there is no public runtime pluginRoot path in activate(ctx).
  • command and args are 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.json and 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.