flockbay 0.10.20 → 0.10.22

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.
@@ -6,8 +6,8 @@ var node_crypto = require('node:crypto');
6
6
  var os = require('node:os');
7
7
  var path = require('node:path');
8
8
  var fs$2 = require('node:fs/promises');
9
- var types = require('./types-BYHCKlu_.cjs');
10
- var index = require('./index-D_mglYG0.cjs');
9
+ var types = require('./types-Z2OYpI8c.cjs');
10
+ var index = require('./index-DQTqwzYd.cjs');
11
11
  var node_child_process = require('node:child_process');
12
12
  var sdk = require('@agentclientprotocol/sdk');
13
13
  var fs = require('fs');
@@ -45,6 +45,17 @@ const KNOWN_TOOL_PATTERNS = {
45
45
  save_memory: ["save_memory", "save-memory"],
46
46
  think: ["think"]
47
47
  };
48
+ const MCP_FLOCKBAY_TOOL_RE = /\bmcp__flockbay__[a-z0-9_]+\b/gi;
49
+ const UNREAL_MCP_TOOL_RE = /\bunreal_mcp_[a-z0-9_]+\b/gi;
50
+ function extractFirstToolNameFromText(text) {
51
+ if (!text) return null;
52
+ const lower = text.toLowerCase();
53
+ const mcpMatch = lower.match(MCP_FLOCKBAY_TOOL_RE);
54
+ if (mcpMatch && mcpMatch[0]) return mcpMatch[0];
55
+ const unrealMatch = lower.match(UNREAL_MCP_TOOL_RE);
56
+ if (unrealMatch && unrealMatch[0]) return `mcp__flockbay__${unrealMatch[0]}`;
57
+ return null;
58
+ }
48
59
  function isInvestigationTool(toolCallId, toolKind) {
49
60
  return toolCallId.includes("codebase_investigator") || toolCallId.includes("investigator") || typeof toolKind === "string" && toolKind.includes("investigator");
50
61
  }
@@ -57,6 +68,18 @@ function extractToolNameFromId(toolCallId) {
57
68
  }
58
69
  }
59
70
  }
71
+ const uuidLike = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(toolCallId);
72
+ if (!uuidLike) {
73
+ const prefix = toolCallId.split(/[-:]/, 1)[0] || "";
74
+ const trimmed = prefix.trim();
75
+ if (trimmed.length >= 2 && trimmed.length <= 160) {
76
+ const hasNonHexAlpha = /[g-z]/i.test(trimmed) || /[A-Z]/.test(trimmed) || /_/.test(trimmed);
77
+ const saneChars = /^[A-Za-z0-9_]+$/.test(trimmed);
78
+ if (saneChars && hasNonHexAlpha) {
79
+ return trimmed;
80
+ }
81
+ }
82
+ }
60
83
  return null;
61
84
  }
62
85
  function determineToolName(toolName, toolCallId, input, params, context) {
@@ -67,20 +90,30 @@ function determineToolName(toolName, toolCallId, input, params, context) {
67
90
  if (idToolName) {
68
91
  return idToolName;
69
92
  }
93
+ const idTextToolName = extractFirstToolNameFromText(toolCallId);
94
+ if (idTextToolName) {
95
+ return idTextToolName;
96
+ }
70
97
  if (input && typeof input === "object") {
71
- const inputStr = JSON.stringify(input).toLowerCase();
98
+ const inputStr = JSON.stringify(input);
99
+ const extracted = extractFirstToolNameFromText(inputStr);
100
+ if (extracted) return extracted;
101
+ const inputLower = inputStr.toLowerCase();
72
102
  for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
73
103
  for (const pattern of patterns) {
74
- if (inputStr.includes(pattern.toLowerCase())) {
104
+ if (inputLower.includes(pattern.toLowerCase())) {
75
105
  return toolName2;
76
106
  }
77
107
  }
78
108
  }
79
109
  }
80
- const paramsStr = JSON.stringify(params).toLowerCase();
110
+ const paramsStr = JSON.stringify(params);
111
+ const paramsExtracted = extractFirstToolNameFromText(paramsStr);
112
+ if (paramsExtracted) return paramsExtracted;
113
+ const paramsLower = paramsStr.toLowerCase();
81
114
  for (const [toolName2, patterns] of Object.entries(KNOWN_TOOL_PATTERNS)) {
82
115
  for (const pattern of patterns) {
83
- if (paramsStr.includes(pattern.toLowerCase())) {
116
+ if (paramsLower.includes(pattern.toLowerCase())) {
84
117
  return toolName2;
85
118
  }
86
119
  }
@@ -317,7 +350,7 @@ class AcpSdkBackend {
317
350
  this.emit({
318
351
  type: "status",
319
352
  status: "error",
320
- detail: "Model not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite"
353
+ detail: "Model not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro-preview, gemini-3-flash-preview"
321
354
  });
322
355
  } else if (/authentication required/i.test(text) || /login required/i.test(text) || /invalid.*(api key|key)/i.test(text) || /permission denied/i.test(text)) {
323
356
  const excerpt = this.stderrExcerpt(20);
@@ -1820,7 +1853,7 @@ class GeminiPermissionHandler {
1820
1853
  const decision = result.decision;
1821
1854
  const reason = result.reason;
1822
1855
  const kind = decision === "approved" || decision === "approved_for_session" ? "policy_allow" : decision === "abort" && reason === "permission_prompt_required" ? "policy_prompt" : "policy_block";
1823
- const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by policy.";
1856
+ const summary = kind === "policy_allow" ? "Allowed." : kind === "policy_prompt" ? "Waiting for permission to run this tool." : reason ? `Blocked: ${reason}` : "Blocked by Policy.";
1824
1857
  const nextSteps = [];
1825
1858
  if (kind === "policy_block" && typeof reason === "string") {
1826
1859
  if (reason.includes("docs_index_read") || reason.includes("Documentation index")) nextSteps.push("Call `mcp__flockbay__docs_index_read`.");
@@ -2372,6 +2405,65 @@ async function runGemini(opts) {
2372
2405
  seen: /* @__PURE__ */ new Set(),
2373
2406
  inAutoReview: false
2374
2407
  };
2408
+ const conversationTurns = [];
2409
+ const MAX_TURNS_TO_KEEP = 80;
2410
+ const pushConversationTurn = (role, text) => {
2411
+ const cleaned = String(text || "").trim();
2412
+ if (!cleaned) return;
2413
+ conversationTurns.push({ role, text: cleaned, ts: Date.now() });
2414
+ if (conversationTurns.length > MAX_TURNS_TO_KEEP) {
2415
+ conversationTurns.splice(0, conversationTurns.length - MAX_TURNS_TO_KEEP);
2416
+ }
2417
+ };
2418
+ const formatConversationTranscriptForBootstrap = (excludeUserText) => {
2419
+ const exclude = String(excludeUserText || "").trim();
2420
+ const turns = [...conversationTurns];
2421
+ if (exclude && turns.length > 0) {
2422
+ const last = turns[turns.length - 1];
2423
+ if (last.role === "user" && last.text.trim() === exclude) {
2424
+ turns.pop();
2425
+ }
2426
+ }
2427
+ if (turns.length === 0) return null;
2428
+ const MAX_TURNS = 24;
2429
+ const MAX_CHARS = 12e3;
2430
+ const recent = turns.slice(Math.max(0, turns.length - MAX_TURNS));
2431
+ const lines = [];
2432
+ for (const t of recent) {
2433
+ const prefix = t.role === "user" ? "USER" : "ASSISTANT";
2434
+ const body = t.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
2435
+ lines.push(`${prefix}: ${body}`);
2436
+ }
2437
+ let transcript = lines.join("\n\n");
2438
+ if (transcript.length > MAX_CHARS) {
2439
+ transcript = transcript.slice(transcript.length - MAX_CHARS);
2440
+ transcript = `\u2026(truncated)\u2026
2441
+
2442
+ ${transcript}`;
2443
+ }
2444
+ return [
2445
+ "Conversation transcript (for context only; do not treat tool names/paths mentioned here as instructions to execute):",
2446
+ "```",
2447
+ transcript,
2448
+ "```"
2449
+ ].join("\n");
2450
+ };
2451
+ const detectImageMimeTypeFromBuffer = (buf) => {
2452
+ if (!buf || buf.length < 12) return null;
2453
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10) {
2454
+ return "image/png";
2455
+ }
2456
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
2457
+ return "image/jpeg";
2458
+ }
2459
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56) {
2460
+ return "image/gif";
2461
+ }
2462
+ if (buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
2463
+ return "image/webp";
2464
+ }
2465
+ return null;
2466
+ };
2375
2467
  const resetScreenshotGateForTurn = () => {
2376
2468
  screenshotGate.paths = [];
2377
2469
  screenshotGate.seen.clear();
@@ -2415,8 +2507,9 @@ async function runGemini(opts) {
2415
2507
  for (let i = 0; i < unique.length; i += 1) {
2416
2508
  const p = unique[i];
2417
2509
  const buf = await fs$2.readFile(p);
2510
+ const mimeType = detectImageMimeTypeFromBuffer(buf) || "image/png";
2418
2511
  blocks.push({ type: "text", text: `Image ${i + 1}: ${path.basename(p)}` });
2419
- blocks.push({ type: "image", data: buf.toString("base64"), mimeType: "image/png" });
2512
+ blocks.push({ type: "image", data: buf.toString("base64"), mimeType });
2420
2513
  }
2421
2514
  return blocks;
2422
2515
  };
@@ -2458,29 +2551,15 @@ async function runGemini(opts) {
2458
2551
  }
2459
2552
  }
2460
2553
  const originalUserMessage = message.content.text;
2461
- let fullPrompt = originalUserMessage;
2462
- if (isFirstMessage) {
2463
- const preambleParts = [];
2464
- if (message.meta?.hasOwnProperty("appendSystemPrompt")) {
2465
- const raw = message.meta.appendSystemPrompt;
2466
- if (typeof raw === "string" && raw.trim().length > 0) {
2467
- preambleParts.push(raw);
2468
- } else {
2469
- preambleParts.push(index.PLATFORM_SYSTEM_PROMPT);
2470
- }
2471
- } else {
2472
- preambleParts.push(index.PLATFORM_SYSTEM_PROMPT);
2473
- }
2474
- if (projectCapsule) preambleParts.push(projectCapsule);
2475
- const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
2476
- fullPrompt = preamble.length > 0 ? preamble + "\n\n" + originalUserMessage : originalUserMessage;
2477
- isFirstMessage = false;
2478
- }
2554
+ pushConversationTurn("user", originalUserMessage);
2555
+ const appendSystemPrompt = message.meta?.hasOwnProperty("appendSystemPrompt") && typeof message.meta.appendSystemPrompt === "string" ? message.meta.appendSystemPrompt : null;
2556
+ const fullPrompt = originalUserMessage;
2479
2557
  const mode = {
2480
2558
  permissionMode: messagePermissionMode || "default",
2481
2559
  model: messageModel,
2482
- originalUserMessage
2560
+ originalUserMessage,
2483
2561
  // Store original message separately
2562
+ appendSystemPrompt
2484
2563
  };
2485
2564
  const promptText = images.length > 0 ? index.withUserImagesMarker(fullPrompt, images.length) : fullPrompt;
2486
2565
  messageQueue.push(promptText, mode, images.length > 0 ? { isolate: true } : void 0);
@@ -2490,19 +2569,14 @@ async function runGemini(opts) {
2490
2569
  const keepAliveInterval = setInterval(() => {
2491
2570
  session.keepAlive(thinking, "remote");
2492
2571
  }, 2e3);
2493
- let isFirstMessage = true;
2494
2572
  const autoPrompt = String(process.env.FLOCKBAY_AUTO_PROMPT || "").trim();
2495
2573
  const autoExitOnIdle = String(process.env.FLOCKBAY_AUTO_EXIT_ON_IDLE || "").trim() === "1";
2496
2574
  if (autoPrompt) {
2497
- const preambleParts = [index.PLATFORM_SYSTEM_PROMPT];
2498
- if (projectCapsule) preambleParts.push(projectCapsule);
2499
- const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
2500
- const fullPrompt = preamble.length > 0 ? preamble + "\n\n" + autoPrompt : autoPrompt;
2501
- isFirstMessage = false;
2502
- messageQueue.push(fullPrompt, {
2575
+ messageQueue.push(autoPrompt, {
2503
2576
  permissionMode: "default",
2504
2577
  model: void 0,
2505
- originalUserMessage: autoPrompt
2578
+ originalUserMessage: autoPrompt,
2579
+ appendSystemPrompt: null
2506
2580
  });
2507
2581
  }
2508
2582
  const sendReady = () => {
@@ -2517,6 +2591,30 @@ async function runGemini(opts) {
2517
2591
  types.logger.debug("[Gemini] Failed to send ready push", pushError);
2518
2592
  }
2519
2593
  };
2594
+ let pendingTurnCompletion = null;
2595
+ const beginTurnCompletionWait = () => {
2596
+ if (pendingTurnCompletion) {
2597
+ types.logger.debug("[Gemini] beginTurnCompletionWait called while a turn is already pending; replacing pending wait");
2598
+ try {
2599
+ pendingTurnCompletion.resolve("error");
2600
+ } catch {
2601
+ }
2602
+ pendingTurnCompletion = null;
2603
+ }
2604
+ let resolve2;
2605
+ const promise = new Promise((res) => {
2606
+ resolve2 = res;
2607
+ });
2608
+ pendingTurnCompletion = { sawRunning: false, resolve: resolve2, promise };
2609
+ return promise;
2610
+ };
2611
+ const maybeResolveTurnCompletion = (status) => {
2612
+ if (!pendingTurnCompletion) return;
2613
+ if (!pendingTurnCompletion.sawRunning) return;
2614
+ const pending = pendingTurnCompletion;
2615
+ pendingTurnCompletion = null;
2616
+ pending.resolve(status);
2617
+ };
2520
2618
  const emitReadyIfIdle = () => {
2521
2619
  if (shouldExit) {
2522
2620
  return false;
@@ -2544,10 +2642,36 @@ async function runGemini(opts) {
2544
2642
  let wasSessionCreated = false;
2545
2643
  let abortNote = null;
2546
2644
  let abortNoteSentToSession = false;
2645
+ let abortRequested = false;
2646
+ let readySentForAbort = false;
2647
+ function normalizeCancelNote(input) {
2648
+ if (typeof input === "string") return input.trim() ? input.trim() : null;
2649
+ if (!input || typeof input !== "object") return null;
2650
+ const note = typeof input.note === "string" ? String(input.note).trim() : "";
2651
+ if (note) return note;
2652
+ const reason = typeof input.reason === "string" ? String(input.reason).trim() : "";
2653
+ if (reason) return reason;
2654
+ return null;
2655
+ }
2656
+ function makeAbortError(message = "Aborted") {
2657
+ const error = new Error(message);
2658
+ error.name = "AbortError";
2659
+ return error;
2660
+ }
2661
+ function abortable(signal, promise) {
2662
+ if (signal.aborted) return Promise.reject(makeAbortError());
2663
+ return new Promise((resolve2, reject) => {
2664
+ const onAbort = () => reject(makeAbortError());
2665
+ signal.addEventListener("abort", onAbort, { once: true });
2666
+ promise.then(resolve2, reject).finally(() => signal.removeEventListener("abort", onAbort));
2667
+ });
2668
+ }
2547
2669
  async function handleAbort(note, options) {
2548
2670
  types.logger.debug("[Gemini] Abort requested - stopping current task");
2549
- if (typeof note === "string" && note.trim()) {
2550
- abortNote = note.trim();
2671
+ abortRequested = true;
2672
+ const normalizedNote = normalizeCancelNote(note);
2673
+ if (normalizedNote) {
2674
+ abortNote = normalizedNote;
2551
2675
  abortNoteSentToSession = Boolean(options?.alreadySentToSession);
2552
2676
  }
2553
2677
  session.sendCodexMessage({
@@ -2558,6 +2682,13 @@ async function runGemini(opts) {
2558
2682
  diffProcessor.reset();
2559
2683
  try {
2560
2684
  abortController.abort();
2685
+ if (pendingTurnCompletion) {
2686
+ try {
2687
+ pendingTurnCompletion.resolve("error");
2688
+ } catch {
2689
+ }
2690
+ pendingTurnCompletion = null;
2691
+ }
2561
2692
  messageQueue.reset();
2562
2693
  if (geminiBackend && acpSessionId) {
2563
2694
  await geminiBackend.cancel(acpSessionId);
@@ -2696,6 +2827,7 @@ async function runGemini(opts) {
2696
2827
  };
2697
2828
  let accumulatedResponse = "";
2698
2829
  let isResponseInProgress = false;
2830
+ let currentResponseMessageId = null;
2699
2831
  function setupGeminiMessageHandler(backend) {
2700
2832
  backend.onMessage((msg) => {
2701
2833
  switch (msg.type) {
@@ -2725,6 +2857,9 @@ async function runGemini(opts) {
2725
2857
  if (msg.status === "running") {
2726
2858
  thinking = true;
2727
2859
  session.keepAlive(thinking, "remote");
2860
+ if (pendingTurnCompletion) {
2861
+ pendingTurnCompletion.sawRunning = true;
2862
+ }
2728
2863
  session.sendCodexMessage({
2729
2864
  type: "task_started",
2730
2865
  id: node_crypto.randomUUID()
@@ -2765,14 +2900,18 @@ async function runGemini(opts) {
2765
2900
  types.logger.debug(`[gemini] Sending complete message to mobile (length: ${finalMessageText.length}): ${finalMessageText.substring(0, 100)}...`);
2766
2901
  types.logger.debug(`[gemini] Full message payload:`, JSON.stringify(messagePayload, null, 2));
2767
2902
  session.sendCodexMessage(messagePayload);
2903
+ pushConversationTurn("assistant", finalMessageText);
2768
2904
  accumulatedResponse = "";
2769
2905
  isResponseInProgress = false;
2770
2906
  }
2907
+ maybeResolveTurnCompletion(msg.status);
2771
2908
  } else if (msg.status === "error") {
2772
2909
  thinking = false;
2773
2910
  session.keepAlive(thinking, "remote");
2774
2911
  accumulatedResponse = "";
2775
2912
  isResponseInProgress = false;
2913
+ currentResponseMessageId = null;
2914
+ maybeResolveTurnCompletion("error");
2776
2915
  const errorMessage = stringifyErrorish(msg.detail || "Unknown error");
2777
2916
  messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
2778
2917
  session.sendCodexMessage({
@@ -2969,6 +3108,7 @@ async function runGemini(opts) {
2969
3108
  if (!message) {
2970
3109
  break;
2971
3110
  }
3111
+ let startedSessionThisTurn = false;
2972
3112
  if (wasSessionCreated && currentModeHash && message.hash !== currentModeHash) {
2973
3113
  types.logger.debug("[Gemini] Mode changed \u2013 restarting Gemini session");
2974
3114
  messageBuffer.addMessage("\u2550".repeat(40), "status");
@@ -2995,8 +3135,9 @@ async function runGemini(opts) {
2995
3135
  const actualModel = determineGeminiModel(modelToUse, localConfigForModel);
2996
3136
  types.logger.debug(`[gemini] Model change - modelToUse=${modelToUse}, actualModel=${actualModel}`);
2997
3137
  types.logger.debug("[gemini] Starting new ACP session with model:", actualModel);
2998
- const { sessionId } = await geminiBackend.startSession();
3138
+ const { sessionId } = await abortable(abortController.signal, geminiBackend.startSession());
2999
3139
  acpSessionId = sessionId;
3140
+ startedSessionThisTurn = true;
3000
3141
  types.logger.debug(`[gemini] New ACP session started: ${acpSessionId}`);
3001
3142
  types.logger.debug(`[gemini] Calling updateDisplayedModel with: ${actualModel}`);
3002
3143
  updateDisplayedModel(actualModel, false);
@@ -3011,6 +3152,15 @@ async function runGemini(opts) {
3011
3152
  let retryThisTurn = false;
3012
3153
  let skipAutoFinalize = false;
3013
3154
  try {
3155
+ const turnSignal = abortController.signal;
3156
+ const sendPromptAndWait = async (prompt) => {
3157
+ if (!geminiBackend || !acpSessionId) {
3158
+ throw new Error("Gemini backend or session not initialized");
3159
+ }
3160
+ const completion = beginTurnCompletionWait();
3161
+ await abortable(turnSignal, geminiBackend.sendPrompt(acpSessionId, prompt));
3162
+ return await abortable(turnSignal, completion);
3163
+ };
3014
3164
  if (first || !wasSessionCreated) {
3015
3165
  if (!geminiBackend) {
3016
3166
  await refreshGeminiCloudToken("before-backend-create");
@@ -3035,8 +3185,9 @@ async function runGemini(opts) {
3035
3185
  if (!acpSessionId) {
3036
3186
  types.logger.debug("[gemini] Starting ACP session...");
3037
3187
  updatePermissionMode(message.mode.permissionMode);
3038
- const { sessionId } = await geminiBackend.startSession();
3188
+ const { sessionId } = await abortable(turnSignal, geminiBackend.startSession());
3039
3189
  acpSessionId = sessionId;
3190
+ startedSessionThisTurn = true;
3040
3191
  types.logger.debug(`[gemini] ACP session started: ${acpSessionId}`);
3041
3192
  wasSessionCreated = true;
3042
3193
  currentModeHash = message.hash;
@@ -3054,26 +3205,48 @@ async function runGemini(opts) {
3054
3205
  }
3055
3206
  const promptToSend = message.message;
3056
3207
  const parsedPrompt = index.extractUserImagesMarker(promptToSend);
3057
- const promptText = parsedPrompt.text;
3058
3208
  const promptImages = parsedPrompt.hasImages ? index.getLatestUserImages().slice(0, parsedPrompt.count) : [];
3209
+ const originalUserMessage = (message.mode?.originalUserMessage || parsedPrompt.text || "").trim();
3210
+ let promptText = parsedPrompt.text;
3211
+ if (startedSessionThisTurn) {
3212
+ const override = typeof message.mode?.appendSystemPrompt === "string" ? message.mode.appendSystemPrompt.trim() : "";
3213
+ const systemPrompt = override.length > 0 ? override : index.PLATFORM_SYSTEM_PROMPT;
3214
+ const preambleParts = [systemPrompt];
3215
+ if (projectCapsule) preambleParts.push(projectCapsule);
3216
+ const transcript = formatConversationTranscriptForBootstrap(originalUserMessage);
3217
+ if (transcript) preambleParts.push(transcript);
3218
+ const preamble = preambleParts.map((p) => String(p || "").trim()).filter(Boolean).join("\n\n");
3219
+ promptText = preamble.length > 0 ? `${preamble}
3220
+
3221
+ ${originalUserMessage}` : originalUserMessage;
3222
+ }
3059
3223
  if (promptImages.length > 0) {
3060
3224
  const blocks = [
3061
3225
  { type: "text", text: promptText },
3062
3226
  ...promptImages.map((img) => ({ type: "image", data: img.base64, mimeType: img.mimeType }))
3063
3227
  ];
3064
3228
  types.logger.debug(`[gemini] Sending multimodal prompt blocks (textLength=${promptText.length}, images=${promptImages.length})`);
3065
- await geminiBackend.sendPrompt(acpSessionId, blocks);
3229
+ const status = await sendPromptAndWait(blocks);
3230
+ if (status !== "idle") {
3231
+ skipAutoFinalize = true;
3232
+ }
3066
3233
  } else {
3067
3234
  types.logger.debug(`[gemini] Sending prompt to Gemini (length: ${promptText.length}): ${promptText.substring(0, 100)}...`);
3068
3235
  types.logger.debug(`[gemini] Full prompt: ${promptText}`);
3069
- await geminiBackend.sendPrompt(acpSessionId, promptText);
3236
+ const status = await sendPromptAndWait(promptText);
3237
+ if (status !== "idle") {
3238
+ skipAutoFinalize = true;
3239
+ }
3070
3240
  }
3071
3241
  types.logger.debug("[gemini] Prompt sent successfully");
3072
- if (!screenshotGate.inAutoReview && screenshotGate.paths.length > 0) {
3242
+ if (!skipAutoFinalize && !screenshotGate.inAutoReview && screenshotGate.paths.length > 0) {
3073
3243
  screenshotGate.inAutoReview = true;
3074
3244
  messageBuffer.addMessage("Auto-reviewing screenshots\u2026", "status");
3075
3245
  const blocks = await buildScreenshotReviewBlocks(screenshotGate.paths);
3076
- await geminiBackend.sendPrompt(acpSessionId, blocks);
3246
+ const status = await sendPromptAndWait(blocks);
3247
+ if (status !== "idle") {
3248
+ skipAutoFinalize = true;
3249
+ }
3077
3250
  screenshotGate.inAutoReview = false;
3078
3251
  }
3079
3252
  if (first) {
@@ -3082,14 +3255,21 @@ async function runGemini(opts) {
3082
3255
  } catch (error) {
3083
3256
  types.logger.debug("[gemini] Error in gemini session:", error);
3084
3257
  const isAbortError = error instanceof Error && error.name === "AbortError";
3085
- if (isAbortError) {
3258
+ const treatAsAbort = abortRequested || isAbortError;
3259
+ if (treatAsAbort) {
3086
3260
  skipAutoFinalize = true;
3087
- const note = abortNote || "Aborted by user";
3261
+ readySentForAbort = true;
3262
+ const note = abortNote || "Canceled by user";
3088
3263
  abortNote = null;
3089
3264
  const alreadySent = abortNoteSentToSession;
3090
3265
  abortNoteSentToSession = false;
3266
+ abortRequested = false;
3267
+ accumulatedResponse = "";
3268
+ isResponseInProgress = false;
3269
+ currentResponseMessageId = null;
3091
3270
  messageBuffer.addMessage(note, "status");
3092
3271
  if (!alreadySent) session.sendSessionEvent({ type: "message", message: note });
3272
+ session.sendSessionEvent({ type: "ready" });
3093
3273
  } else {
3094
3274
  const rawErrorString = error instanceof Error ? error.message : String(error);
3095
3275
  const looksLikeAuthOrQuotaError = /unauthenticated|unauthorized|invalid (api )?key|api key not valid|permission denied|forbidden|expired|quota|usage limit|rate limit|resource exhausted|resource_exhausted|status 401|status 403|status 429|\b401\b|\b403\b|\b429\b/i.test(rawErrorString);
@@ -3121,7 +3301,7 @@ async function runGemini(opts) {
3121
3301
  const errorString = String(error);
3122
3302
  if (errorCode === 404 || errorDetails.includes("notFound") || errorDetails.includes("404") || errorMessage.includes("not found") || errorMessage.includes("404")) {
3123
3303
  const currentModel2 = displayedModel || "gemini-2.5-pro";
3124
- errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite`;
3304
+ errorMsg = `Model "${currentModel2}" not found. Available models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-3-pro-preview, gemini-3-flash-preview`;
3125
3305
  } else if (errorCode === 429 || errorDetails.includes("429") || errorMessage.includes("429") || errorString.includes("429") || errorDetails.includes("rateLimitExceeded") || errorDetails.includes("RESOURCE_EXHAUSTED") || errorMessage.includes("Rate limit exceeded") || errorMessage.includes("Resource exhausted") || errorString.includes("rateLimitExceeded") || errorString.includes("RESOURCE_EXHAUSTED")) {
3126
3306
  errorMsg = "Gemini API rate limit exceeded. Please wait a moment and try again. The API will retry automatically.";
3127
3307
  } else if (errorDetails.includes("quota") || errorMessage.includes("quota") || errorString.includes("quota")) {
@@ -3145,6 +3325,7 @@ async function runGemini(opts) {
3145
3325
  permissionHandler.reset();
3146
3326
  reasoningProcessor.abort();
3147
3327
  diffProcessor.reset();
3328
+ abortRequested = false;
3148
3329
  thinking = false;
3149
3330
  session.keepAlive(thinking, "remote");
3150
3331
  if (!retryThisTurn) {
@@ -3174,7 +3355,11 @@ async function runGemini(opts) {
3174
3355
  });
3175
3356
  }
3176
3357
  }
3177
- emitReadyIfIdle();
3358
+ if (readySentForAbort) {
3359
+ readySentForAbort = false;
3360
+ } else {
3361
+ emitReadyIfIdle();
3362
+ }
3178
3363
  }
3179
3364
  types.logger.debug(`[gemini] Main loop: turn completed, continuing to next iteration (queue size: ${messageQueue.size()})`);
3180
3365
  }