paseo-acp-agy 1.2.14 → 1.2.16

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.
@@ -10,6 +10,8 @@ export declare class ACPServer extends EventEmitter {
10
10
  private skipNarration;
11
11
  private rl;
12
12
  private isRunning;
13
+ private pendingRequests;
14
+ private nextRequestId;
13
15
  constructor(options?: {
14
16
  input?: Readable;
15
17
  output?: Writable;
@@ -22,6 +24,7 @@ export declare class ACPServer extends EventEmitter {
22
24
  private sendNotification;
23
25
  private sendSuccess;
24
26
  private sendError;
27
+ sendRequest<T = any>(method: string, params: unknown): Promise<T>;
25
28
  private publishCommands;
26
29
  private publishUsageUpdate;
27
30
  private sessionState;
@@ -29,6 +29,8 @@ export class ACPServer extends EventEmitter {
29
29
  skipNarration;
30
30
  rl = null;
31
31
  isRunning = false;
32
+ pendingRequests = new Map();
33
+ nextRequestId = 1;
32
34
  constructor(options = {}) {
33
35
  super();
34
36
  this.input = options.input || process.stdin;
@@ -98,6 +100,13 @@ export class ACPServer extends EventEmitter {
98
100
  };
99
101
  this.send(response);
100
102
  }
103
+ sendRequest(method, params) {
104
+ return new Promise((resolve, reject) => {
105
+ const id = this.nextRequestId++;
106
+ this.pendingRequests.set(id, { resolve, reject });
107
+ this.send({ jsonrpc: "2.0", id, method, params });
108
+ });
109
+ }
101
110
  publishCommands(sessionId) {
102
111
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
103
112
  sessionId,
@@ -198,6 +207,20 @@ export class ACPServer extends EventEmitter {
198
207
  const isNotification = id === undefined || id === null;
199
208
  const params = (parsed.params || {});
200
209
  logger.debug("Received ACP message", { method, isNotification, id });
210
+ // Handle responses to requests initiated by the server (e.g. session/request_permission)
211
+ if (id !== undefined && !method) {
212
+ const pending = this.pendingRequests.get(id);
213
+ if (pending) {
214
+ this.pendingRequests.delete(id);
215
+ if (parsed.error) {
216
+ pending.reject(parsed.error);
217
+ }
218
+ else {
219
+ pending.resolve(parsed.result);
220
+ }
221
+ return;
222
+ }
223
+ }
201
224
  try {
202
225
  switch (method) {
203
226
  case ACP_METHODS.INITIALIZE: {
@@ -559,13 +582,70 @@ export class ACPServer extends EventEmitter {
559
582
  rawInput: toolCall.rawInput,
560
583
  },
561
584
  });
562
- if (toolCall.questionCard) {
563
- this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
585
+ if (toolName === "ask_question") {
586
+ const rawParams = step.tool_info.parameters || {};
587
+ const rawQuestions = Array.isArray(rawParams.questions)
588
+ ? rawParams.questions
589
+ : [];
590
+ const options = [];
591
+ for (let qIdx = 0; qIdx < rawQuestions.length; qIdx++) {
592
+ const q = rawQuestions[qIdx];
593
+ const opts = Array.isArray(q.options) ? q.options : [];
594
+ for (let optIdx = 0; optIdx < opts.length; optIdx++) {
595
+ const opt = opts[optIdx];
596
+ const clean = typeof opt === "string" ? opt.replace(/^\(Recommended\)\s*/i, "").trim() : String(opt);
597
+ const isRec = typeof opt === "string" && (opt.includes("(Recommended)") || opt.includes("(Recomendado)"));
598
+ options.push({
599
+ optionId: `q${qIdx + 1}_opt${optIdx + 1}`,
600
+ name: `${clean}${isRec ? " (Recomendado)" : ""}`,
601
+ kind: "allow_once",
602
+ });
603
+ }
604
+ }
605
+ options.push({
606
+ optionId: "cancel",
607
+ name: "Cancelar",
608
+ kind: "reject_once",
609
+ });
610
+ const promptText = rawQuestions.map((q) => q.question).filter(Boolean).join(" / ") || "Pergunta do Assistente";
611
+ this.sendRequest("session/request_permission", {
564
612
  sessionId: session.id,
565
- update: {
566
- sessionUpdate: "agent_message_chunk",
567
- content: { type: "text", text: `\n\n${toolCall.questionCard}\n\n` },
613
+ toolCall: {
614
+ toolCallId: toolCall.toolCallId,
615
+ title: toolCall.title,
616
+ kind: "question",
617
+ content: [{ type: "text", text: promptText }],
618
+ rawInput: toolCall.rawInput,
568
619
  },
620
+ options,
621
+ questions: rawQuestions,
622
+ }).then((permRes) => {
623
+ logger.info("Interactive question answered in Paseo", { permRes });
624
+ const outcome = permRes?.outcome;
625
+ let answerText = "";
626
+ if (outcome?._meta?.answers && typeof outcome._meta.answers === "object") {
627
+ answerText = Object.entries(outcome._meta.answers)
628
+ .map(([k, v]) => `• **${k}**: ${v}`)
629
+ .join("\n");
630
+ }
631
+ else if (outcome?.optionId && outcome.optionId !== "cancel") {
632
+ const matched = options.find((o) => o.optionId === outcome.optionId);
633
+ answerText = matched ? matched.name : outcome.optionId;
634
+ }
635
+ if (answerText) {
636
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
637
+ sessionId: session.id,
638
+ update: {
639
+ sessionUpdate: "agent_message_chunk",
640
+ content: {
641
+ type: "text",
642
+ text: `\n\n> 💡 **Opção Selecionada:**\n${answerText}\n\n`,
643
+ },
644
+ },
645
+ });
646
+ }
647
+ }).catch((err) => {
648
+ logger.debug("Interactive question cancelled or dismissed", { error: err.message });
569
649
  });
570
650
  }
571
651
  }
@@ -1195,6 +1195,63 @@ ${braceIndent}}`;
1195
1195
  }
1196
1196
  }
1197
1197
  }
1198
+ // Patch mapPermissionRequest for native QuestionFormCard support in Paseo UI
1199
+ if (!acpCode.includes("snapshot?.kind === \"question\" || params?.kind === \"question\"")) {
1200
+ const oldMapPermRegex = /function\s+mapPermissionRequest\s*\([^)]*\)\s*\{[\s\S]*?const\s+kind\s*=\s*snapshot\.kind\s*===\s*"switch_mode"\s*\?\s*"mode"\s*:\s*"tool";/m;
1201
+ const newMapPerm = `function mapPermissionRequest(provider, requestId, params, snapshot) {
1202
+ if (snapshot?.kind === "question" || params?.kind === "question" || (params?.questions && Array.isArray(params.questions)) || (snapshot?.rawInput?.questions && Array.isArray(snapshot?.rawInput?.questions))) {
1203
+ const rawQ = params.questions || snapshot?.rawInput?.questions || [];
1204
+ const questions = rawQ.map((q, idx) => ({
1205
+ header: q.header || q.title || ("Question " + (idx + 1)),
1206
+ question: q.question || q.prompt || "Escolha uma opção:",
1207
+ options: (q.options || []).map((opt) => typeof opt === "string" ? {
1208
+ label: opt.replace(/^\\(Recommended\\)\\s*/i, "").replace(/^\\(Recomendado\\)\\s*/i, "").trim(),
1209
+ description: (opt.includes("(Recommended)") || opt.includes("(Recomendado)")) ? "Recomendado" : undefined
1210
+ } : { label: opt.label || String(opt), description: opt.description }),
1211
+ multiSelect: q.multiSelect === true || q.is_multi_select === true || q.multiple === true,
1212
+ allowOther: true,
1213
+ allowEmpty: false,
1214
+ }));
1215
+ return {
1216
+ id: requestId,
1217
+ provider,
1218
+ name: "question",
1219
+ kind: "question",
1220
+ title: params.toolCall?.title ?? snapshot?.title ?? "Pergunta do Assistente",
1221
+ input: { questions },
1222
+ actions: (params.options || []).map((option) => ({
1223
+ id: option.optionId,
1224
+ label: option.name,
1225
+ behavior: option.kind.startsWith("allow") ? "allow" : "deny",
1226
+ })),
1227
+ metadata: {
1228
+ toolCallId: params.toolCall?.toolCallId,
1229
+ rawRequest: params,
1230
+ options: params.options,
1231
+ },
1232
+ };
1233
+ }
1234
+ const kind = snapshot.kind === "switch_mode" ? "mode" : "tool";`;
1235
+ if (oldMapPermRegex.test(acpCode)) {
1236
+ acpCode = acpCode.replace(oldMapPermRegex, newMapPerm);
1237
+ acpModified = true;
1238
+ changes.push(`Patched ${acpFile} for interactive QuestionFormCard support`);
1239
+ }
1240
+ }
1241
+ // Patch respondToPermission for QuestionFormCard updatedInput.answers
1242
+ if (!acpCode.includes("response.updatedInput?.answers")) {
1243
+ const oldRespondMatch = acpCode.match(/this\.pendingPermissions\.delete\(requestId\);[\s\S]*?pending\.resolve\([\s\S]*?\);\s*\r?\n\s*this\.pushEvent/m);
1244
+ if (oldRespondMatch) {
1245
+ const newRespond = `this.pendingPermissions.delete(requestId);
1246
+ const outcome = response.updatedInput?.answers
1247
+ ? { outcome: "selected", optionId: selectedOption?.optionId || "form_submit", _meta: { answers: response.updatedInput.answers } }
1248
+ : (selectedOption ? { outcome: { outcome: "selected", optionId: selectedOption.optionId } } : { outcome: { outcome: "cancelled" } });
1249
+ pending.resolve(response.updatedInput?.answers ? { outcome } : (selectedOption ? { outcome: { outcome: "selected", optionId: selectedOption.optionId } } : { outcome: { outcome: "cancelled" } }));
1250
+ this.pushEvent`;
1251
+ acpCode = acpCode.replace(oldRespondMatch[0], newRespond);
1252
+ acpModified = true;
1253
+ }
1254
+ }
1198
1255
  if (acpModified) {
1199
1256
  fs.writeFileSync(acpFile, acpCode, "utf-8");
1200
1257
  changes.push(`Patched ${acpFile} for context-window token telemetry and usage updates`);
@@ -135,7 +135,7 @@ export interface SessionInfo {
135
135
  updatedAt?: string | null;
136
136
  _meta?: Record<string, unknown> | null;
137
137
  }
138
- export type ToolKind = "read" | "edit" | "execute" | "fetch" | "search" | "other";
138
+ export type ToolKind = "read" | "edit" | "execute" | "fetch" | "search" | "question" | "other";
139
139
  export declare function mapToolNameToKind(toolName: string): ToolKind;
140
140
  export declare function extractPromptText(promptInput: unknown): string;
141
141
  export interface ModelDefinition {
@@ -102,7 +102,7 @@ export function normalizeToolCall(toolName, rawParams = {}, stepIndex) {
102
102
  return {
103
103
  toolCallId,
104
104
  title: "❓ Pergunta do Assistente",
105
- kind: "other",
105
+ kind: "question",
106
106
  rawInput: normalizedParams,
107
107
  questionCard: questionCard || undefined,
108
108
  };
@@ -187,24 +187,36 @@ export function extractPlanEntries(text) {
187
187
  }
188
188
  /**
189
189
  * Formats Antigravity's ask_question structure into a clear, responsive markdown card.
190
+ * Supports multiple questions in a single call and single/multi-choice selection modes.
190
191
  */
191
192
  function formatQuestionCard(questions) {
192
193
  if (!questions || questions.length === 0)
193
194
  return "";
195
+ const validQuestions = questions.filter((q) => q && typeof q === "object");
196
+ const total = validQuestions.length;
194
197
  const cards = [];
195
- for (const q of questions) {
196
- if (!q || typeof q !== "object")
197
- continue;
198
+ for (let i = 0; i < total; i++) {
199
+ const q = validQuestions[i];
198
200
  const prompt = q.question || "Escolha uma opção:";
199
201
  const options = Array.isArray(q.options) ? q.options : [];
200
- let card = `### ❓ Pergunta do Assistente\n> **${prompt}**\n\n`;
202
+ const isMulti = Boolean(q.is_multi_select);
203
+ const titlePrefix = total > 1
204
+ ? `### ❓ Pergunta ${i + 1} de ${total}`
205
+ : `### ❓ Pergunta do Assistente`;
206
+ let card = `${titlePrefix}\n> **${prompt}**\n\n`;
201
207
  if (options.length > 0) {
202
- card += `*Responda indicando o número da opção desejada:*\n`;
208
+ if (isMulti) {
209
+ card += `*☑️ Múltipla escolha (você pode selecionar mais de uma opção, ex: 1, 3):*\n`;
210
+ }
211
+ else {
212
+ card += `*🔘 Escolha única (selecione uma das opções abaixo):*\n`;
213
+ }
203
214
  card += options
204
215
  .map((opt, idx) => {
205
216
  const isRec = opt.includes("(Recommended)") || opt.includes("(Recomendado)");
206
217
  const clean = opt.replace(/^\(Recommended\)\s*/i, "").replace(/^\(Recomendado\)\s*/i, "");
207
- return `- **[${idx + 1}]** ${clean}${isRec ? " *(Recomendado)*" : ""}`;
218
+ const mark = isMulti ? "- [ ]" : "-";
219
+ return `${mark} **[${idx + 1}]** ${clean}${isRec ? " *(Recomendado)*" : ""}`;
208
220
  })
209
221
  .join("\n");
210
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.2.14",
3
+ "version": "1.2.16",
4
4
  "description": "ACP (Agent Client Protocol) provider for Google Antigravity in Paseo and Zed",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",