mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
feat(pi): add programmatic plan mode event (#898)
This commit is contained in:
@@ -63,6 +63,25 @@ When the agent calls `plannotator_submit_plan`, the Plannotator UI opens in your
|
||||
|
||||
The agent iterates on the plan until you approve, then executes with full tool access. On resubmission, Plan Diff highlights what changed since the previous version.
|
||||
|
||||
### Programmatic plan-mode control
|
||||
|
||||
Other Pi extensions can enter, exit, toggle, or query Plannotator plan mode through the shared Pi event bus without invoking the `/plannotator` slash command:
|
||||
|
||||
```ts
|
||||
import { PLANNOTATOR_REQUEST_CHANNEL } from "@plannotator/pi-extension/plannotator-events";
|
||||
|
||||
const response = await new Promise((resolve) => {
|
||||
pi.events.emit(PLANNOTATOR_REQUEST_CHANNEL, {
|
||||
requestId: crypto.randomUUID(),
|
||||
action: "plan-mode",
|
||||
payload: { mode: "enter" }, // "enter" | "exit" | "toggle" | "status"
|
||||
respond: resolve,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
A handled response returns the resulting phase, for example `{ status: "handled", result: { phase: "planning" } }`.
|
||||
|
||||
### Configuring per-phase behavior
|
||||
|
||||
Plannotator loads configuration in three layers:
|
||||
|
||||
@@ -211,7 +211,21 @@ function sendUserMessageWithCurrentSessionFallback(
|
||||
export default function plannotator(pi: ExtensionAPI): void {
|
||||
const currentPiSession = registerCurrentPiSession(pi);
|
||||
let phase: Phase = "idle";
|
||||
void registerPlannotatorEventListeners(pi);
|
||||
void registerPlannotatorEventListeners(pi, {
|
||||
handlePlanMode: async (mode, ctx) => {
|
||||
if (mode === "status") return { phase };
|
||||
if (mode === "enter") {
|
||||
if (phase === "idle") await enterPlanning(ctx);
|
||||
return { phase };
|
||||
}
|
||||
if (mode === "exit") {
|
||||
if (phase !== "idle") await exitToIdle(ctx);
|
||||
return { phase };
|
||||
}
|
||||
await togglePlanMode(ctx);
|
||||
return { phase };
|
||||
},
|
||||
});
|
||||
let lastSubmittedPath: string | null = null;
|
||||
let checklistItems: ChecklistItem[] = [];
|
||||
let savedState: SavedPhaseState | null = null;
|
||||
|
||||
@@ -22,6 +22,7 @@ export const PLANNOTATOR_REVIEW_RESULT_CHANNEL = "plannotator:review-result" as
|
||||
export const PLANNOTATOR_TIMEOUT_MS = 5_000;
|
||||
|
||||
export type PlannotatorAction =
|
||||
| "plan-mode"
|
||||
| "plan-review"
|
||||
| "review-status"
|
||||
| "code-review"
|
||||
@@ -56,6 +57,14 @@ export interface PlannotatorRequestBase<A extends PlannotatorAction, P, R> {
|
||||
respond: (response: PlannotatorResponse<R>) => void;
|
||||
}
|
||||
|
||||
export interface PlannotatorPlanModePayload {
|
||||
mode?: "enter" | "exit" | "toggle" | "status";
|
||||
}
|
||||
|
||||
export interface PlannotatorPlanModeResult {
|
||||
phase: "idle" | "planning" | "executing";
|
||||
}
|
||||
|
||||
export interface PlannotatorPlanReviewPayload {
|
||||
planFilePath?: string;
|
||||
planContent: string;
|
||||
@@ -127,6 +136,7 @@ export interface PlannotatorArchiveResult {
|
||||
}
|
||||
|
||||
export type PlannotatorRequestMap = {
|
||||
"plan-mode": PlannotatorRequestBase<"plan-mode", PlannotatorPlanModePayload, PlannotatorPlanModeResult>;
|
||||
"plan-review": PlannotatorRequestBase<"plan-review", PlannotatorPlanReviewPayload, PlannotatorPlanReviewStartResult>;
|
||||
"review-status": PlannotatorRequestBase<"review-status", PlannotatorReviewStatusPayload, PlannotatorReviewStatusResult>;
|
||||
"code-review": PlannotatorRequestBase<"code-review", PlannotatorCodeReviewPayload, PlannotatorCodeReviewResult>;
|
||||
@@ -136,6 +146,7 @@ export type PlannotatorRequestMap = {
|
||||
};
|
||||
export type PlannotatorRequest = PlannotatorRequestMap[PlannotatorAction];
|
||||
export type PlannotatorResponseMap = {
|
||||
"plan-mode": PlannotatorResponse<PlannotatorPlanModeResult>;
|
||||
"plan-review": PlannotatorResponse<PlannotatorPlanReviewStartResult>;
|
||||
"review-status": PlannotatorResponse<PlannotatorReviewStatusResult>;
|
||||
"code-review": PlannotatorResponse<PlannotatorCodeReviewResult>;
|
||||
@@ -145,6 +156,7 @@ export type PlannotatorResponseMap = {
|
||||
};
|
||||
function isPlannotatorAction(value: unknown): value is PlannotatorAction {
|
||||
return (
|
||||
value === "plan-mode" ||
|
||||
value === "plan-review" ||
|
||||
value === "review-status" ||
|
||||
value === "code-review" ||
|
||||
@@ -200,7 +212,17 @@ function createActiveSessionContext() {
|
||||
};
|
||||
}
|
||||
|
||||
export function registerPlannotatorEventListeners(pi: ExtensionAPI): void {
|
||||
export interface PlannotatorEventListenerOptions {
|
||||
handlePlanMode?: (
|
||||
mode: NonNullable<PlannotatorPlanModePayload["mode"]>,
|
||||
ctx: ExtensionContext,
|
||||
) => Promise<PlannotatorPlanModeResult> | PlannotatorPlanModeResult;
|
||||
}
|
||||
|
||||
export function registerPlannotatorEventListeners(
|
||||
pi: ExtensionAPI,
|
||||
options: PlannotatorEventListenerOptions = {},
|
||||
): void {
|
||||
const activeSessionContext = createActiveSessionContext();
|
||||
|
||||
// Plannotator event requests are handled against the latest active session.
|
||||
@@ -233,6 +255,20 @@ export function registerPlannotatorEventListeners(pi: ExtensionAPI): void {
|
||||
}
|
||||
|
||||
switch (request.action) {
|
||||
case "plan-mode": {
|
||||
if (!options.handlePlanMode) {
|
||||
request.respond({ status: "unavailable", error: "Plan mode control is not available in this session." });
|
||||
return;
|
||||
}
|
||||
const mode = request.payload?.mode ?? "toggle";
|
||||
if (mode !== "enter" && mode !== "exit" && mode !== "toggle" && mode !== "status") {
|
||||
request.respond({ status: "error", error: "Invalid plan-mode payload.mode." });
|
||||
return;
|
||||
}
|
||||
const result = await options.handlePlanMode(mode, ctx);
|
||||
request.respond({ status: "handled", result });
|
||||
return;
|
||||
}
|
||||
case "plan-review": {
|
||||
const planContent = request.payload?.planContent;
|
||||
if (typeof planContent !== "string" || !planContent.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user