@vitos-pizza/vitos-pizza 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to vitos-pizza are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.1] - 2026-07-12
8
+
9
+ ### Added
10
+
11
+ - **Multi-select questions** — `question` tool `selectType: "multi"` (Space toggle, Enter submit)
12
+ - **Multi-question tabs** — `questions: [...]` for tabbed prompts (Tab/←→ switch; per-tab single or multi)
13
+
14
+ ### Changed
15
+
16
+ - **Subagent hypa-only reads** — scout / planner / worker drop builtin `read`/`grep`/`find`/`ls`; exploration uses `hypa_*` only (`bash`/`edit`/`write` unchanged)
17
+ - Plan mode footer: **Worth considering** (gaps/adjacent capabilities) replaces **Next** (no mode-switch verbs)
18
+ - Subagent question forwarding: subprocess children use **file channel only** (skip in-process RPC)
19
+
20
+ ### Fixed
21
+
22
+ - **Subagent question false cancel** — child no-UI RPC no longer replies `cancelled`; parent TUI receives forwarded prompts
23
+ - **Resize recovery** — after Windows/Cursor reports `columns`/`rows` as 0 (or the border TUI is not ready yet), retry invalidate+full redraw for up to 1s so the UI recovers when size settles without another resize event
24
+
7
25
  ## [0.4.0] - 2026-07-11
8
26
 
9
27
  ### Added
@@ -50,7 +68,7 @@ All notable changes to vitos-pizza are documented here.
50
68
 
51
69
  - **`@vitos-pizza/todoist`** — in-memory task list with TUI widget, `/todo` command, and LLM tools (`todo_add` / `todo_update` / `todo_complete` / `todo_delete`)
52
70
  - Terminal **resize recovery** in ui-enhancements — debounce + full redraw after resize (fixes blank/corrupt TUI on Windows)
53
- - Plan-mode **Next** footer guidance and optional scout (skip recon when context is enough; parallel scouts for independent areas)
71
+ - Plan-mode **Worth considering** footer (gaps/adjacent capabilities; not next-steps) and optional scout (skip recon when context is enough; parallel scouts for independent areas)
54
72
 
55
73
  ### Changed
56
74
 
@@ -70,15 +88,13 @@ All notable changes to vitos-pizza are documented here.
70
88
  - Pi distribution layout: repo root as single install entry
71
89
  - `scripts/sync-pi-manifest.mjs` module assembler
72
90
  - Original logo at `assets/logo.svg`
91
+ - Built-in modules: agent-mode, question, session-title, permission-system, subagents, keybindings, ui-enhancements, websearch
92
+ - Agent modes: `agent` / `plan` / `execute` with `/mode` and cycle shortcuts
93
+ - Structured `question` tool with subagent parent-session forwarding
94
+ - Session auto-title from first meaningful user message
73
95
 
74
- ### Removed
75
-
76
- - Nested `packages/xin-pi` aggregator
77
- - `@xin-pi/core`, template scaffold, postinstall symlink hack
78
-
79
- ### Publishing
96
+ ### Acknowledgments
80
97
 
81
- - GitHub Actions publishes `@vitos-pizza/vitos-pizza` via **npm Trusted Publishing (OIDC)** — no long-lived `NPM_TOKEN`
82
- - Workflow: `.github/workflows/publish.yml` (triggers on `v*` tags / `workflow_dispatch`)
83
- - `scripts/prepare-publish.mjs` rewrites `file:` workspace deps for the npm tarball
84
- - First package version must be published once manually (or as a stub) before Trusted Publisher can be configured on npmjs.com
98
+ - [Pi](https://pi.dev/) / [@earendil-works/pi-coding-agent](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)
99
+ - [@gotgenes/pi-permission-system](https://www.npmjs.com/package/@gotgenes/pi-permission-system)
100
+ - Pi official `question` and subagent examples
package/README.md CHANGED
@@ -16,7 +16,7 @@ Inspired by [Vito's Pizzeria](https://garfield.fandom.com/wiki/Vito_Cappelletti)
16
16
 
17
17
  ![vitos-pizza architecture](./assets/architecture.png)
18
18
 
19
- Typical flow: **plan** → optional **scout** → **planner** → confirm → **execute** / **worker**. Shell output is compressed via Hypa bash rewrite; prefer `hypa_*` read tools when exploring.
19
+ Typical flow: **plan** → optional **scout** → **planner** → confirm → **execute** / **worker**. Shell output is compressed via Hypa bash rewrite; scout/planner/worker explore with `hypa_*` only (main session may still use builtin reads).
20
20
 
21
21
  | Capability | Module | What you get |
22
22
  |------------|--------|--------------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitos-pizza/vitos-pizza",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Vito's Pizzeria — a Pi distribution (Garfield-approved)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -39,17 +39,17 @@
39
39
  "dependencies": {
40
40
  "@hypabolic/pi-hypa": "^0.1.10",
41
41
  "typebox": "^1.1.38",
42
- "@vitos-pizza/agent-mode": "0.4.0",
43
- "@vitos-pizza/hypa": "0.4.0",
44
- "@vitos-pizza/keybindings": "0.4.0",
45
- "@vitos-pizza/permission-system": "0.4.0",
46
- "@vitos-pizza/question": "0.4.0",
47
- "@vitos-pizza/session-title": "0.4.0",
48
- "@vitos-pizza/settings-preset": "0.4.0",
49
- "@vitos-pizza/subagents": "0.4.0",
50
- "@vitos-pizza/todoist": "0.4.0",
51
- "@vitos-pizza/ui-enhancements": "0.4.0",
52
- "@vitos-pizza/websearch": "0.4.0"
42
+ "@vitos-pizza/agent-mode": "0.4.1",
43
+ "@vitos-pizza/hypa": "0.4.1",
44
+ "@vitos-pizza/keybindings": "0.4.1",
45
+ "@vitos-pizza/permission-system": "0.4.1",
46
+ "@vitos-pizza/question": "0.4.1",
47
+ "@vitos-pizza/session-title": "0.4.1",
48
+ "@vitos-pizza/settings-preset": "0.4.1",
49
+ "@vitos-pizza/subagents": "0.4.1",
50
+ "@vitos-pizza/todoist": "0.4.1",
51
+ "@vitos-pizza/ui-enhancements": "0.4.1",
52
+ "@vitos-pizza/websearch": "0.4.1"
53
53
  },
54
54
  "bundledDependencies": [
55
55
  "@hypabolic/pi-hypa",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitos-pizza/agent-mode",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Centralized agent / plan / execute mode switching for Vito's Pizzeria",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,8 +13,8 @@
13
13
  "lint:fix": "biome check --write ."
14
14
  },
15
15
  "dependencies": {
16
- "@vitos-pizza/keybindings": "0.4.0",
17
- "@vitos-pizza/permission-system": "0.4.0"
16
+ "@vitos-pizza/keybindings": "0.4.1",
17
+ "@vitos-pizza/permission-system": "0.4.1"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@earendil-works/pi-coding-agent": "*"
@@ -28,26 +28,33 @@ This supersedes conflicting skill or prior guidance about implementing code or c
28
28
 
29
29
  **Exploration**
30
30
  - Scout is optional — only for unfamiliar or multi-file recon. Parallel scouts via \`subagent({ tasks: [...] })\` for independent areas, then planner.
31
- - Prefer \`hypa_read\` / \`hypa_grep\` / \`hypa_find\` / \`hypa_ls\` when available; fall back to read/grep/find/ls.
31
+ - Scout/planner use \`hypa_read\` / \`hypa_grep\` / \`hypa_find\` / \`hypa_ls\` only. Main session: prefer hypa_*; fall back to read/grep/find/ls if needed.
32
32
  - Web tools only for current external facts outside the repo.
33
33
 
34
34
  **Delegation**
35
35
  - Enough context → \`subagent({ agent: "planner", task })\`
36
36
  - Need recon → \`subagent({ chain: [scout, planner] })\`
37
37
 
38
- **Optional Next footer** (2–3 one-line reply verbs) when the next step is not obvious:
38
+ **Optional open-questions footer** (2–4 one-line prompts) only when the plan may miss scope or adjacent capabilities the user has not clarified. Soft guidance — not next-steps, not mode-switching:
39
39
 
40
40
  \`\`\`
41
- **Next**
42
- - Confirm \`/mode execute\` (or Ctrl+. / Alt+M)
43
- - Say what to change if scope is off
44
- \`\`\``;
41
+ **Worth considering**
42
+ - <gap or adjacent capability the user may not have decided yet>?
43
+ - <in/out of scope this round>?
44
+ \`\`\`
45
+
46
+ Rules:
47
+ - Ask about omissions, edge cases, related features, or "do you also need X?"
48
+ - Do NOT list mode-switching or how-to-proceed verbs (e.g. \`/mode execute\`, Ctrl+.)
49
+ - Do NOT restate the plan as todos
50
+ - Prefer \`question\` for mutually exclusive choices; use this footer for soft, optional prompts
51
+ - Skip the footer when scope is already clear`;
45
52
 
46
53
  export const PLAN_MODE_MESSAGE = `[PLAN MODE ACTIVE]
47
54
 
48
55
  Read-only planning. No write/edit/bash/worker.
49
56
  Clarify with \`question\` when needed. Scout only if recon is required; otherwise planner.
50
- Prefer hypa_* read tools when available. Return the plan and wait; optionally a short **Next** footer.`;
57
+ Prefer hypa_* read tools when available. Return the plan and wait; optionally a short **Worth considering** footer for gaps/adjacent capabilities.`;
51
58
 
52
59
  export const PLAN_MODE_ENDED_MESSAGE = `[PLAN MODE ENDED]
53
60
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitos-pizza/hypa",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Vito's Pizzeria Hypa defaults — additive mode, MCP proxy off, config seed",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitos-pizza/keybindings",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Centralized keyboard shortcut registry for Vito's Pizzeria Pi distribution",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitos-pizza/permission-system",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Permission enforcement for Vito's Pizzeria Pi distribution",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "lint:fix": "biome check --write ."
19
19
  },
20
20
  "dependencies": {
21
- "@vitos-pizza/keybindings": "0.4.0",
21
+ "@vitos-pizza/keybindings": "0.4.1",
22
22
  "tree-sitter-bash": "^0.25.1",
23
23
  "web-tree-sitter": "^0.25.10",
24
24
  "zod": "^3.25.76"
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * @vitos-pizza/question — structured ask-user question tool.
3
+ * Supports single-question (legacy) and multi-question tabbed modes.
3
4
  */
4
5
 
5
6
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
7
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
8
  import { startQuestionFileWatcher } from "../src/forwarding/file-watcher.ts";
8
9
  import { startQuestionRpcServer } from "../src/forwarding/forwarder.ts";
10
+ import type { ForwardQuestionPayload } from "../src/forwarding/forwarder.ts";
9
11
  import {
10
12
  promptQuestionLocally,
11
13
  registerQuestionTool,
12
14
  } from "../src/register-tool.ts";
15
+ import type { MultiQuestionParams, QuestionParams } from "../src/types.ts";
13
16
 
14
17
  export default function (
15
18
  pi: import("@earendil-works/pi-coding-agent").ExtensionAPI,
@@ -18,24 +21,42 @@ export default function (
18
21
  let stopFileWatcher: (() => void) | null = null;
19
22
  const agentDir = getAgentDir();
20
23
 
21
- const unsubRpc = startQuestionRpcServer(pi.events, async (payload) => {
22
- const ctx = activeCtx;
23
- if (!ctx?.hasUI) {
24
- return {
25
- question: payload.question,
26
- options: payload.options.map((o) => o.label),
27
- answer: null,
28
- cancelled: true,
29
- responderSessionId: ctx?.sessionManager.getSessionId?.() ?? "unknown",
30
- respondedAt: Date.now(),
31
- };
32
- }
33
- return promptQuestionLocally(
34
- ctx.ui,
35
- { question: payload.question, options: payload.options },
36
- ctx.sessionManager.getSessionId?.() ?? "unknown",
37
- );
38
- });
24
+ const unsubRpc = startQuestionRpcServer(
25
+ pi.events,
26
+ async (payload: ForwardQuestionPayload) => {
27
+ const ctx = activeCtx;
28
+ // No UI or wrong session — stay silent so callers can fall through
29
+ // to file-based forwarding (do not fake cancelled).
30
+ if (!ctx?.hasUI) return null;
31
+
32
+ const sessionId =
33
+ ctx.sessionManager.getSessionId?.() ?? "unknown";
34
+ if (
35
+ payload.targetSessionId &&
36
+ payload.targetSessionId !== sessionId
37
+ ) {
38
+ return null;
39
+ }
40
+
41
+ if (payload.questions) {
42
+ return promptQuestionLocally(
43
+ ctx.ui,
44
+ { questions: payload.questions } as MultiQuestionParams,
45
+ sessionId,
46
+ );
47
+ }
48
+
49
+ return promptQuestionLocally(
50
+ ctx.ui,
51
+ {
52
+ question: payload.question ?? "",
53
+ options: payload.options ?? [],
54
+ selectType: payload.selectType,
55
+ } as QuestionParams,
56
+ sessionId,
57
+ );
58
+ },
59
+ );
39
60
 
40
61
  registerQuestionTool(pi, {
41
62
  getAgentDir: () => agentDir,
@@ -54,16 +75,28 @@ export default function (
54
75
  responderSessionId: sessionId,
55
76
  showQuestion: async (params) => {
56
77
  if (!activeCtx?.hasUI) {
78
+ if ("questions" in params) {
79
+ return {
80
+ answers: {},
81
+ cancelled: true,
82
+ responderSessionId: sessionId,
83
+ respondedAt: Date.now(),
84
+ };
85
+ }
57
86
  return {
58
- question: params.question,
59
- options: params.options.map((o) => o.label),
87
+ question: (params as QuestionParams).question,
88
+ options: (params as QuestionParams).options.map((o) => o.label),
60
89
  answer: null,
61
90
  cancelled: true,
62
91
  responderSessionId: sessionId,
63
92
  respondedAt: Date.now(),
64
93
  };
65
94
  }
66
- return promptQuestionLocally(activeCtx.ui, params, sessionId);
95
+ return promptQuestionLocally(
96
+ activeCtx.ui,
97
+ params,
98
+ sessionId,
99
+ );
67
100
  },
68
101
  });
69
102
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitos-pizza/question",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Structured ask-user question tool for Vito's Pizzeria Pi distribution",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import type {
12
12
  ForwardedQuestionRequest,
13
13
  ForwardedQuestionResponse,
14
+ MultiQuestionParams,
14
15
  QuestionParams,
15
16
  } from "../types.ts";
16
17
  import { getQuestionForwardingRoot } from "./forwarder.ts";
@@ -21,7 +22,9 @@ export function startQuestionFileWatcher(options: {
21
22
  agentDir: string;
22
23
  sessionId: string;
23
24
  responderSessionId: string;
24
- showQuestion: (params: QuestionParams) => Promise<ForwardedQuestionResponse>;
25
+ showQuestion: (
26
+ params: QuestionParams | MultiQuestionParams,
27
+ ) => Promise<ForwardedQuestionResponse>;
25
28
  pollIntervalMs?: number;
26
29
  }): () => void {
27
30
  const forwardingRoot = getQuestionForwardingRoot(
@@ -62,16 +65,43 @@ export function startQuestionFileWatcher(options: {
62
65
  readFileSync(processingPath, "utf8"),
63
66
  ) as ForwardedQuestionRequest;
64
67
 
65
- const response = await options.showQuestion({
66
- question: request.question,
67
- options: request.options,
68
- });
69
-
70
- writeFileSync(
71
- join(responsesDir, `${requestId}.json`),
72
- JSON.stringify(response),
73
- "utf8",
74
- );
68
+ // Determine single vs multi
69
+ if (request.questions && request.questions.length > 0) {
70
+ const response = await options.showQuestion({
71
+ questions: request.questions,
72
+ });
73
+ writeFileSync(
74
+ join(responsesDir, `${requestId}.json`),
75
+ JSON.stringify(response),
76
+ "utf8",
77
+ );
78
+ } else if (request.question !== undefined && request.options) {
79
+ const response = await options.showQuestion({
80
+ question: request.question,
81
+ options: request.options,
82
+ selectType: request.selectType,
83
+ });
84
+ writeFileSync(
85
+ join(responsesDir, `${requestId}.json`),
86
+ JSON.stringify(response),
87
+ "utf8",
88
+ );
89
+ } else {
90
+ // Unknown format — write failure
91
+ const failure: ForwardedQuestionResponse = {
92
+ question: "",
93
+ options: [],
94
+ answer: null,
95
+ cancelled: true,
96
+ responderSessionId: options.responderSessionId,
97
+ respondedAt: Date.now(),
98
+ };
99
+ writeFileSync(
100
+ join(responsesDir, `${requestId}.json`),
101
+ JSON.stringify(failure),
102
+ "utf8",
103
+ );
104
+ }
75
105
  } catch {
76
106
  const failure: ForwardedQuestionResponse = {
77
107
  question: "",
@@ -7,11 +7,15 @@ import {
7
7
  writeFileSync,
8
8
  } from "node:fs";
9
9
  import { join } from "node:path";
10
+ import { isSubagentChild } from "../parent-session.ts";
10
11
  import type {
11
12
  ForwardedQuestionRequest,
12
13
  ForwardedQuestionResponse,
14
+ MultiQuestionParams,
13
15
  QuestionOption,
14
16
  QuestionParams,
17
+ QuestionTab,
18
+ SelectType,
15
19
  } from "../types.ts";
16
20
  import { QUESTION_RPC_PROMPT, questionReplyChannel } from "./channels.ts";
17
21
 
@@ -20,6 +24,22 @@ interface EventBus {
20
24
  on(channel: string, handler: (payload: unknown) => void): () => void;
21
25
  }
22
26
 
27
+ /**
28
+ * Payload sent over RPC / file for a forwarded question prompt.
29
+ * Supports both single- and multi-question modes.
30
+ */
31
+ export interface ForwardQuestionPayload {
32
+ requestId: string;
33
+ targetSessionId: string;
34
+ requesterSessionId: string;
35
+ /** Single‑question mode */
36
+ question?: string;
37
+ options?: QuestionOption[];
38
+ selectType?: SelectType;
39
+ /** Multi‑question mode */
40
+ questions?: QuestionTab[];
41
+ }
42
+
23
43
  export function getQuestionForwardingRoot(
24
44
  agentDir: string,
25
45
  sessionId: string,
@@ -43,23 +63,37 @@ export async function forwardQuestionPrompt(options: {
43
63
  agentDir?: string;
44
64
  targetSessionId: string;
45
65
  requesterSessionId: string;
46
- params: QuestionParams;
66
+ params: QuestionParams | MultiQuestionParams;
47
67
  timeoutMs?: number;
48
68
  }): Promise<ForwardedQuestionResponse | null> {
49
69
  const requestId = randomUUID();
50
- const payload = {
70
+
71
+ const isMulti = "questions" in options.params;
72
+ const payload: ForwardQuestionPayload = {
51
73
  requestId,
52
74
  targetSessionId: options.targetSessionId,
53
75
  requesterSessionId: options.requesterSessionId,
54
- question: options.params.question,
55
- options: options.params.options,
56
76
  };
57
77
 
58
- if (options.events) {
78
+ if (isMulti) {
79
+ const mp = options.params as MultiQuestionParams;
80
+ payload.questions = mp.questions;
81
+ } else {
82
+ const sp = options.params as QuestionParams;
83
+ payload.question = sp.question;
84
+ payload.options = sp.options;
85
+ payload.selectType = sp.selectType;
86
+ }
87
+
88
+ // Subprocess children have an isolated event bus; their own no-UI RPC
89
+ // server would poison-reply with cancelled. Skip RPC and use files only.
90
+ if (options.events && !isSubagentChild()) {
91
+ // Same-process RPC. If no capable listener replies, fall through to
92
+ // file-based forwarding quickly instead of waiting the full timeout.
59
93
  const rpcReply = await waitForRpcReply(
60
94
  options.events,
61
95
  payload,
62
- options.timeoutMs ?? 600_000,
96
+ 1500, // short RPC-specific timeout
63
97
  );
64
98
  if (rpcReply) return rpcReply;
65
99
  }
@@ -74,7 +108,7 @@ export async function forwardQuestionPrompt(options: {
74
108
  targetSessionId: options.targetSessionId,
75
109
  params: options.params,
76
110
  },
77
- options.timeoutMs ?? 600_000,
111
+ options.timeoutMs ?? 300_000, // 5 min for user response
78
112
  );
79
113
  }
80
114
 
@@ -83,13 +117,7 @@ export async function forwardQuestionPrompt(options: {
83
117
 
84
118
  function waitForRpcReply(
85
119
  events: EventBus,
86
- payload: {
87
- requestId: string;
88
- targetSessionId: string;
89
- requesterSessionId: string;
90
- question: string;
91
- options: QuestionOption[];
92
- },
120
+ payload: ForwardQuestionPayload,
93
121
  timeoutMs: number,
94
122
  ): Promise<ForwardedQuestionResponse | null> {
95
123
  return new Promise((resolve) => {
@@ -121,20 +149,31 @@ function waitForFileReply(
121
149
  requestId: string;
122
150
  requesterSessionId: string;
123
151
  targetSessionId: string;
124
- params: QuestionParams;
152
+ params: QuestionParams | MultiQuestionParams;
125
153
  },
126
154
  timeoutMs: number,
127
155
  ): Promise<ForwardedQuestionResponse | null> {
128
156
  const forwardingRoot = getQuestionForwardingRoot(agentDir, targetSessionId);
129
157
  const { requestsDir, responsesDir } = getForwardingDirs(forwardingRoot);
158
+
159
+ const isMulti = "questions" in payload.params;
130
160
  const request: ForwardedQuestionRequest = {
131
161
  id: payload.requestId,
132
162
  createdAt: Date.now(),
133
163
  requesterSessionId: payload.requesterSessionId,
134
164
  targetSessionId: payload.targetSessionId,
135
- question: payload.params.question,
136
- options: payload.params.options,
137
165
  };
166
+
167
+ if (isMulti) {
168
+ const mp = payload.params as MultiQuestionParams;
169
+ request.questions = mp.questions;
170
+ } else {
171
+ const sp = payload.params as QuestionParams;
172
+ request.question = sp.question;
173
+ request.options = sp.options;
174
+ request.selectType = sp.selectType;
175
+ }
176
+
138
177
  writeFileSync(
139
178
  join(requestsDir, `${request.id}.json`),
140
179
  JSON.stringify(request),
@@ -169,19 +208,14 @@ function waitForFileReply(
169
208
 
170
209
  export function startQuestionRpcServer(
171
210
  events: EventBus,
172
- handler: (payload: {
173
- requestId: string;
174
- question: string;
175
- options: QuestionOption[];
176
- }) => Promise<ForwardedQuestionResponse>,
211
+ handler: (
212
+ payload: ForwardQuestionPayload,
213
+ ) => Promise<ForwardedQuestionResponse | null | undefined>,
177
214
  ): () => void {
178
215
  return events.on(QUESTION_RPC_PROMPT, async (raw) => {
179
- const payload = raw as {
180
- requestId: string;
181
- question: string;
182
- options: QuestionOption[];
183
- };
216
+ const payload = raw as ForwardQuestionPayload;
184
217
  const response = await handler(payload);
218
+ if (response == null) return;
185
219
  events.emit(questionReplyChannel(payload.requestId), response);
186
220
  });
187
221
  }