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

243 lines
8.7 KiB
Markdown

# Commands And Feedback
This document is the normative SDK contract for plugin commands, including
commands that open a view or 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 human menu-click UI presents minimal host-owned feedback for `skipped`
and `failed` results. It intentionally does not expose the raw result payload
or assume which tool (Git, ADB, Gradle, and so on) the plugin uses.
For successful actions and richer status, open or focus a plugin layout/window
from the handler with
`ctx.services.windows.open({ layoutType, state })`.
## Preconditions
Check preconditions before calling `runCommand()`. These are task-launch
preconditions, not prerequisites for opening an explanatory view:
- `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` do not resolve package-relative plugin resources
implicitly. Build an explicit absolute script path from `ctx.pluginRoot` when
launching read-only files shipped in the installed package.
- Do not use `ctx.pluginRoot` as `cwd`; `cwd` remains relative to the project root.
- `command` and `args` are separate values. Do not shell-join user input.
A command whose purpose is to open or focus a diagnostic/configuration view
should call `services.windows.open(...)` before probing optional executables.
Missing Git, ADB, Gradle, or another tool must not block that view when the view
can explain the degraded state and the next step. Probe tools from the view (or
from its data loader) and render the degraded state there.
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: "opened"; message?: string; alreadyOpen: boolean }
| { status: "skipped"; reason: string; message: string }
| { status: "failed"; reason?: string; message?: string }
| { status: "launched"; taskId: string; state: string; message: string };
```
Use the same `status` vocabulary consistently:
- `opened`: the requested view was opened or focused. `alreadyOpen: true` is a
successful focus operation, not a failure or skip.
- `skipped`: no background task was created.
- `launched`: a task was created; inspect the task state for later progress.
- `failed`: the handler caught a failure and returned structured failure
feedback. An uncaught/rejected handler is also treated as a failure by the
host.
Visible surfaces are intentionally distinct:
- Command return value: immediate feedback for programmatic callers and agents.
The human menu host maps `skipped` and `failed` results to concise feedback;
plugin-specific fields and messages are not displayed verbatim. `opened`
results, including `alreadyOpen: true`, succeed without an alert. The host
logs structured diagnostics with `pluginId`, `commandId`, `status`, `reason`
and `message`; raw objects, stack traces and internal ids are not user-facing.
- 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;
```
The `opened` branch above must remain successful even when an optional tool
diagnosed by `HealthView` is absent. If `windows.open(...)` itself rejects, log
the technical cause and either let the promise reject or return `failed`; the
host will show generic, plugin-neutral feedback rather than the raw message.
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.
Do not try to reach packaged plugin scripts through workspace-relative paths:
```ts
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.