@vitos-pizza/vitos-pizza 0.3.1 → 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.
Files changed (37) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +42 -11
  3. package/README.md +31 -4
  4. package/assets/architecture.png +0 -0
  5. package/assets/architecture.svg +97 -0
  6. package/package.json +22 -12
  7. package/packages/agent-mode/package.json +3 -3
  8. package/packages/agent-mode/src/plan-instructions.ts +19 -7
  9. package/packages/hypa/extensions/index.ts +16 -0
  10. package/packages/hypa/package.json +23 -0
  11. package/packages/hypa/scripts/smoke.mjs +50 -0
  12. package/packages/hypa/src/config.ts +37 -0
  13. package/packages/hypa/src/seed.ts +22 -0
  14. package/packages/hypa/tsconfig.json +13 -0
  15. package/packages/keybindings/package.json +1 -1
  16. package/packages/permission-system/package.json +2 -2
  17. package/packages/permission-system/presets/default.json +8 -0
  18. package/packages/permission-system/presets/plan.json +6 -0
  19. package/packages/permission-system/presets/yolo.json +1 -0
  20. package/packages/question/extensions/index.ts +54 -21
  21. package/packages/question/package.json +1 -1
  22. package/packages/question/src/forwarding/file-watcher.ts +41 -11
  23. package/packages/question/src/forwarding/forwarder.ts +61 -27
  24. package/packages/question/src/question-ui.ts +773 -78
  25. package/packages/question/src/register-tool.ts +351 -40
  26. package/packages/question/src/types.ts +79 -7
  27. package/packages/session-title/package.json +3 -3
  28. package/packages/settings-preset/package.json +1 -1
  29. package/packages/subagents/agents/planner.md +3 -2
  30. package/packages/subagents/agents/scout.md +2 -1
  31. package/packages/subagents/agents/worker.md +3 -2
  32. package/packages/subagents/package.json +1 -1
  33. package/packages/todoist/package.json +1 -1
  34. package/packages/ui-enhancements/package.json +1 -1
  35. package/packages/ui-enhancements/src/chrome/resize-recovery.ts +71 -14
  36. package/packages/websearch/package.json +1 -1
  37. package/scripts/sync-pi-manifest.mjs +18 -12
@@ -5,12 +5,18 @@
5
5
  "grep": "allow",
6
6
  "find": "allow",
7
7
  "ls": "allow",
8
+ "hypa_read": "allow",
9
+ "hypa_grep": "allow",
10
+ "hypa_find": "allow",
11
+ "hypa_ls": "allow",
8
12
  "question": "allow",
9
13
  "subagent": "allow",
10
14
  "wait": "allow",
11
15
  "web_search": "ask",
12
16
  "web_read": "ask",
13
17
  "bash": "deny",
18
+ "hypa_shell": "deny",
19
+ "hypa_mcp_proxy": "deny",
14
20
  "write": "deny",
15
21
  "edit": "deny",
16
22
  "skill": {
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "permission": {
3
3
  "*": "allow",
4
+ "hypa_mcp_proxy": "deny",
4
5
  "path": {
5
6
  "*": "allow",
6
7
  "*.env": "deny",
@@ -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.3.1",
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
  }