199 lines
6.3 KiB
Markdown
199 lines
6.3 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
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` and `args` are separate values. Do not shell-join user input.
|
|
|
|
If any required precondition fails, return a skipped result:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```tsx
|
|
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:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
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.
|