Skip to content

Instantly share code, notes, and snippets.

@S1M0N38
Last active May 5, 2026 15:35
Show Gist options
  • Select an option

  • Save S1M0N38/6b247314f7fff33c0a4087b49793def2 to your computer and use it in GitHub Desktop.

Select an option

Save S1M0N38/6b247314f7fff33c0a4087b49793def2 to your computer and use it in GitHub Desktop.

EVENTS.md — pi-ask Event System

Design

pi-ask emits events on pi.events at ask-flow boundaries. Other extensions listen with plain pi.events.on() — no imports from pi-ask needed.

Two events:

Channel Payload When
ask:started { source, title?, questionCount } After validation, before the ask UI opens
ask:completed AskResult (see src/types.ts) After the ask flow closes (submitted, elaborated, or cancelled)

ask:started

{
  source: "tool" | "answer" | "replay";
  title?: string;
  questionCount: number;
}
  • source: "tool" — LLM called the ask_user tool
  • source: "answer" — user ran /answer
  • source: "replay" — user ran /answer:again or /ask:replay

ask:completed

The full AskResult object (already defined in src/types.ts). Consumers check result.cancelled and result.mode:

  • cancelled: true — user dismissed the flow
  • mode: "submit" + cancelled: false — normal submission
  • mode: "elaborate" + cancelled: false — elaboration submission

Consumer example

// In any other extension — no import from pi-ask needed
pi.events.on("ask:started", (data) => {
  console.log(`Ask flow opened: ${data.questionCount} question(s) from ${data.source}`);
});

pi.events.on("ask:completed", (result) => {
  const r = result as { cancelled: boolean; mode: string; answers: Record<string, any> };
  if (r.cancelled) {
    console.log("Ask flow cancelled");
    return;
  }
  console.log("Ask flow completed:", r.mode, r.answers);
});

What does NOT emit

  • Validation failures — no flow opened, nothing to observe
  • Non-interactive calls (ctx.hasUI === false) — no UI flow
  • /answer extraction failures or cancellation — pre-flow step

Implementation

No new files. No threading. No controller changes.

src/ask-tool.ts

Add events to the Pick<ExtensionAPI, ...> and emit inline:

async function executeAskTool(
  pi: Pick<ExtensionAPI, "appendEntry" | "events">,
  toolCallId: string,
  params: AskParams,
  _signal: AbortSignal | undefined,
  _onUpdate: unknown,
  ctx: ExtensionContext
) {
  const validation = validateParams(params);
  if (!validation.ok) {
    return invalidPayloadResponse(params, validation.issues);
  }
  appendAskPayload(pi, { params, source: "tool", sourceEntryId: toolCallId });
  if (!ctx.hasUI) {
    return nonInteractiveResponse(validation.state);
  }
  pi.events.emit("ask:started", {
    source: "tool",
    title: params.title,
    questionCount: params.questions.length,
  });
  ctx.ui.setWorkingVisible(false);
  try {
    const result = await runAskFlow(ctx, params);
    pi.events.emit("ask:completed", result);
    return successfulResponse(result);
  } finally {
    ctx.ui.setWorkingVisible(true);
  }
}

src/answer-commands.ts

Add events to runAskAndSendSubmittedResult and emit inline:

async function runAskAndSendSubmittedResult(
  pi: Pick<ExtensionAPI, "sendUserMessage" | "events">,
  ctx: ExtensionContext,
  params: AskParams,
  options: { allowFreeform: boolean; source: "answer" | "replay" }
): Promise<void> {
  pi.events.emit("ask:started", {
    source: options.source,
    title: params.title,
    questionCount: params.questions.length,
  });
  const result = await withHiddenWorkingRow(ctx, () =>
    runAskFlow(ctx, params, { allowFreeform: options.allowFreeform })
  );
  pi.events.emit("ask:completed", result);
  if (result.cancelled) {
    ctx.ui.notify("Ask form cancelled.", "info");
    return;
  }
  sendAskResult(pi, result, ctx);
}

Callers pass source:

  • runAnswerCommand{ allowFreeform: true, source: "answer" }
  • runReplayCommand{ allowFreeform: ..., source: "replay" }

Files NOT changed

  • src/ui/controller.ts — no threading, no emission, untouched
  • src/state/ — no changes
  • src/types.ts — no new types
  • No new files

Test strategy

Add test cases to tests/ask-tool.test.ts:

  • Widen the mock pi to include events: { emit(), on() }
  • Assert ask:started is emitted with correct payload before the flow
  • Assert ask:completed is emitted with the AskResult after the flow
  • Assert no events on validation failure
  • Assert no events in non-interactive mode

File change summary

File Change
src/ask-tool.ts Widen Pick<> to include "events", emit ask:started and ask:completed
src/answer-commands.ts Widen Pick<> to include "events", add source to options, emit both events
tests/ask-tool.test.ts Widen mock, add emission assertions
docs/contract.md Add "Event bus" section
docs/architecture.md Add event emission invariant
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment