@junctionpanel/server 0.1.31 → 0.1.33

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 (78) hide show
  1. package/dist/server/client/daemon-client.d.ts +5 -1
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +42 -0
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +3 -0
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +88 -5
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  10. package/dist/server/server/agent/agent-projections.js +9 -2
  11. package/dist/server/server/agent/agent-projections.js.map +1 -1
  12. package/dist/server/server/agent/agent-sdk-types.d.ts +19 -3
  13. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  14. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  15. package/dist/server/server/agent/agent-storage.d.ts +30 -30
  16. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  17. package/dist/server/server/agent/agent-storage.js +33 -1
  18. package/dist/server/server/agent/agent-storage.js.map +1 -1
  19. package/dist/server/server/agent/codex-config.d.ts +12 -0
  20. package/dist/server/server/agent/codex-config.d.ts.map +1 -0
  21. package/dist/server/server/agent/codex-config.js +42 -0
  22. package/dist/server/server/agent/codex-config.js.map +1 -0
  23. package/dist/server/server/agent/mcp-server.js +8 -8
  24. package/dist/server/server/agent/mcp-server.js.map +1 -1
  25. package/dist/server/server/agent/provider-launch-config.d.ts +2 -2
  26. package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
  27. package/dist/server/server/agent/provider-launch-config.js +32 -5
  28. package/dist/server/server/agent/provider-launch-config.js.map +1 -1
  29. package/dist/server/server/agent/provider-manifest.js +10 -10
  30. package/dist/server/server/agent/provider-manifest.js.map +1 -1
  31. package/dist/server/server/agent/providers/claude/model-catalog.d.ts +17 -25
  32. package/dist/server/server/agent/providers/claude/model-catalog.d.ts.map +1 -1
  33. package/dist/server/server/agent/providers/claude/model-catalog.js +228 -40
  34. package/dist/server/server/agent/providers/claude/model-catalog.js.map +1 -1
  35. package/dist/server/server/agent/providers/claude-agent.d.ts +2 -1
  36. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  37. package/dist/server/server/agent/providers/claude-agent.js +201 -36
  38. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  39. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +30 -1
  40. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  41. package/dist/server/server/agent/providers/codex-app-server-agent.js +309 -49
  42. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  43. package/dist/server/server/agent/providers/gemini-agent.d.ts +17 -5
  44. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  45. package/dist/server/server/agent/providers/gemini-agent.js +1040 -482
  46. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  47. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  48. package/dist/server/server/agent/providers/opencode-agent.js +1 -1
  49. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  50. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  51. package/dist/server/server/bootstrap.d.ts.map +1 -1
  52. package/dist/server/server/bootstrap.js +2 -1
  53. package/dist/server/server/bootstrap.js.map +1 -1
  54. package/dist/server/server/cli-runtime-exports.d.ts +2 -0
  55. package/dist/server/server/cli-runtime-exports.d.ts.map +1 -1
  56. package/dist/server/server/cli-runtime-exports.js +2 -0
  57. package/dist/server/server/cli-runtime-exports.js.map +1 -1
  58. package/dist/server/server/daemon-doctor.d.ts +4 -0
  59. package/dist/server/server/daemon-doctor.d.ts.map +1 -1
  60. package/dist/server/server/daemon-doctor.js +33 -12
  61. package/dist/server/server/daemon-doctor.js.map +1 -1
  62. package/dist/server/server/daemon-package-context.d.ts +10 -0
  63. package/dist/server/server/daemon-package-context.d.ts.map +1 -0
  64. package/dist/server/server/daemon-package-context.js +31 -0
  65. package/dist/server/server/daemon-package-context.js.map +1 -0
  66. package/dist/server/server/package-update.d.ts +32 -0
  67. package/dist/server/server/package-update.d.ts.map +1 -0
  68. package/dist/server/server/package-update.js +196 -0
  69. package/dist/server/server/package-update.js.map +1 -0
  70. package/dist/server/server/session.d.ts +1 -0
  71. package/dist/server/server/session.d.ts.map +1 -1
  72. package/dist/server/server/session.js +49 -16
  73. package/dist/server/server/session.js.map +1 -1
  74. package/dist/server/shared/messages.d.ts +2150 -1786
  75. package/dist/server/shared/messages.d.ts.map +1 -1
  76. package/dist/server/shared/messages.js +36 -1
  77. package/dist/server/shared/messages.js.map +1 -1
  78. package/package.json +3 -2
@@ -1,23 +1,24 @@
1
- import { execSync, spawn } from "node:child_process";
2
- import { readdir, readFile, realpath, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { homedir, tmpdir } from "node:os";
1
+ import * as acp from "@agentclientprotocol/sdk";
2
+ import { execSync, spawn, spawnSync } from "node:child_process";
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
4
5
  import path from "node:path";
5
- import readline from "node:readline";
6
+ import { Readable as NodeReadable, Writable as NodeWritable } from "node:stream";
6
7
  import { z } from "zod";
7
8
  import { applyProviderEnv, isProviderCommandAvailable, resolveProviderCommandPrefix, } from "../provider-launch-config.js";
8
- import { writeImageAttachment } from "./image-attachments.js";
9
9
  import { ToolEditInputSchema, ToolEditOutputSchema, ToolReadInputSchema, ToolReadOutputSchema, ToolSearchInputSchema, ToolShellInputSchema, ToolShellOutputSchema, ToolWriteInputSchema, ToolWriteOutputSchema, toEditToolDetail, toReadToolDetail, toSearchToolDetail, toShellToolDetail, toWriteToolDetail, } from "./tool-call-detail-primitives.js";
10
10
  import { coerceToolCallId, nonEmptyString } from "./tool-call-mapper-utils.js";
11
11
  const GEMINI_PROVIDER = "gemini";
12
12
  const GEMINI_MODES = ["default", "auto_edit", "yolo", "plan"];
13
13
  const GEMINI_GLOBAL_DIR = path.join(homedir(), ".gemini");
14
14
  const GEMINI_TMP_DIR = path.join(GEMINI_GLOBAL_DIR, "tmp");
15
+ const GEMINI_HISTORY_FALLBACK_IDLE_MS = 1000;
15
16
  const GEMINI_CAPABILITIES = {
16
17
  supportsStreaming: true,
17
18
  supportsSessionPersistence: true,
18
19
  supportsDynamicModes: true,
19
20
  supportsMcpServers: true,
20
- supportsReasoningStream: false,
21
+ supportsReasoningStream: true,
21
22
  supportsToolInvocations: true,
22
23
  };
23
24
  const DEFAULT_GEMINI_MODE = "default";
@@ -91,59 +92,6 @@ const GEMINI_RUNTIME_MODES = [
91
92
  description: "Read-only planning mode",
92
93
  },
93
94
  ];
94
- const GeminiStreamInitSchema = z.object({
95
- type: z.literal("init"),
96
- session_id: z.string().trim().min(1),
97
- model: z.string().trim().min(1).optional(),
98
- });
99
- const GeminiStreamMessageSchema = z.object({
100
- type: z.literal("message"),
101
- role: z.enum(["user", "assistant"]),
102
- content: z.string(),
103
- delta: z.boolean().optional(),
104
- });
105
- const GeminiStreamToolUseSchema = z.object({
106
- type: z.literal("tool_use"),
107
- tool_name: z.string().trim().min(1),
108
- tool_id: z.string().trim().min(1),
109
- parameters: z.unknown().optional(),
110
- });
111
- const GeminiStreamToolResultSchema = z.object({
112
- type: z.literal("tool_result"),
113
- tool_id: z.string().trim().min(1),
114
- status: z.string().trim().min(1),
115
- output: z.unknown().optional(),
116
- error: z.unknown().optional(),
117
- });
118
- const GeminiStreamErrorSchema = z.object({
119
- type: z.literal("error"),
120
- severity: z.string().optional(),
121
- message: z.string().trim().min(1),
122
- });
123
- const GeminiStreamResultSchema = z.object({
124
- type: z.literal("result"),
125
- status: z.string().trim().min(1),
126
- stats: z
127
- .object({
128
- total_tokens: z.number().finite().optional(),
129
- input_tokens: z.number().finite().optional(),
130
- output_tokens: z.number().finite().optional(),
131
- cached: z.number().finite().optional(),
132
- input: z.number().finite().optional(),
133
- duration_ms: z.number().finite().optional(),
134
- tool_calls: z.number().finite().optional(),
135
- })
136
- .passthrough()
137
- .optional(),
138
- });
139
- const GeminiStreamEventSchema = z.discriminatedUnion("type", [
140
- GeminiStreamInitSchema,
141
- GeminiStreamMessageSchema,
142
- GeminiStreamToolUseSchema,
143
- GeminiStreamToolResultSchema,
144
- GeminiStreamErrorSchema,
145
- GeminiStreamResultSchema,
146
- ]);
147
95
  const GeminiRecordedThoughtSchema = z
148
96
  .object({
149
97
  thought: z.string().optional(),
@@ -180,6 +128,54 @@ const GeminiRecordedSessionSchema = z
180
128
  messages: z.array(GeminiRecordedMessageSchema),
181
129
  })
182
130
  .passthrough();
131
+ class AsyncEventQueue {
132
+ constructor() {
133
+ this.items = [];
134
+ this.waiters = [];
135
+ this.closed = false;
136
+ this.error = null;
137
+ }
138
+ push(item) {
139
+ if (this.closed) {
140
+ return;
141
+ }
142
+ this.items.push(item);
143
+ this.waiters.shift()?.();
144
+ }
145
+ close() {
146
+ if (this.closed) {
147
+ return;
148
+ }
149
+ this.closed = true;
150
+ for (const notify of this.waiters.splice(0)) {
151
+ notify();
152
+ }
153
+ }
154
+ fail(error) {
155
+ if (this.closed) {
156
+ return;
157
+ }
158
+ this.error = error;
159
+ this.closed = true;
160
+ for (const notify of this.waiters.splice(0)) {
161
+ notify();
162
+ }
163
+ }
164
+ async shift() {
165
+ while (this.items.length === 0) {
166
+ if (this.closed) {
167
+ if (this.error) {
168
+ throw this.error;
169
+ }
170
+ return null;
171
+ }
172
+ await new Promise((resolve) => {
173
+ this.waiters.push(resolve);
174
+ });
175
+ }
176
+ return this.items.shift() ?? null;
177
+ }
178
+ }
183
179
  function resolveGeminiBinary() {
184
180
  try {
185
181
  const geminiPath = execSync("which gemini", { encoding: "utf8" }).trim();
@@ -192,6 +188,56 @@ function resolveGeminiBinary() {
192
188
  }
193
189
  throw new Error("Gemini CLI not found. Please install gemini globally so Junction can launch the provider.");
194
190
  }
191
+ function assertGeminiAcpSupport(runtimeSettings) {
192
+ const launchPrefix = resolveProviderCommandPrefix(runtimeSettings?.command, resolveGeminiBinary);
193
+ const env = applyProviderEnv(process.env, runtimeSettings);
194
+ const result = spawnSync(launchPrefix.command, [...launchPrefix.args, "--help"], {
195
+ env,
196
+ encoding: "utf8",
197
+ });
198
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
199
+ if (output.includes("--acp")) {
200
+ return;
201
+ }
202
+ throw new Error("Installed Gemini CLI does not support --acp. Upgrade Gemini CLI to a version that includes ACP support.");
203
+ }
204
+ function stringifyUnknown(value) {
205
+ if (typeof value === "string") {
206
+ return value;
207
+ }
208
+ if (value instanceof Error) {
209
+ return value.message;
210
+ }
211
+ try {
212
+ return JSON.stringify(value);
213
+ }
214
+ catch {
215
+ return String(value);
216
+ }
217
+ }
218
+ function asRecord(value) {
219
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
220
+ return null;
221
+ }
222
+ return value;
223
+ }
224
+ function formatGeminiAcpError(error) {
225
+ if (error instanceof acp.RequestError) {
226
+ const directMessage = nonEmptyString(error.message);
227
+ const dataRecord = asRecord(error.data);
228
+ const nestedMessage = nonEmptyString(asRecord(dataRecord?.error)?.message) ??
229
+ nonEmptyString(dataRecord?.message) ??
230
+ null;
231
+ const message = nestedMessage ?? directMessage ?? "Gemini ACP request failed";
232
+ if (error.code === -32002 ||
233
+ message.toLowerCase().includes("authentication required")) {
234
+ return "Gemini CLI authentication is required. Run `gemini` locally and sign in before using Gemini in Junction.";
235
+ }
236
+ return message;
237
+ }
238
+ const message = nonEmptyString(stringifyUnknown(error));
239
+ return message ?? "Gemini ACP request failed";
240
+ }
195
241
  export function normalizeGeminiMode(modeId) {
196
242
  if (!modeId) {
197
243
  return DEFAULT_GEMINI_MODE;
@@ -204,62 +250,35 @@ export function normalizeGeminiMode(modeId) {
204
250
  }
205
251
  throw new Error(`Unknown Gemini mode '${modeId}'. Valid modes: ${GEMINI_MODES.join(", ")}`);
206
252
  }
207
- const GEMINI_IMAGE_ONLY_PROMPT = "The user attached image(s) with no additional text. Inspect them and respond to the images.";
208
- export async function normalizeGeminiPromptInput(prompt, options) {
209
- if (typeof prompt === "string") {
210
- return prompt;
211
- }
212
- const writeAttachment = options?.writeImageAttachment ?? writeImageAttachment;
213
- const textBlocks = [];
214
- const imageReferences = [];
215
- for (const block of prompt) {
216
- if (block.type === "text") {
217
- textBlocks.push(block.text);
218
- continue;
219
- }
220
- try {
221
- const filePath = await writeAttachment(block.mimeType, block.data);
222
- imageReferences.push(`@${filePath}`);
223
- }
224
- catch (error) {
225
- const message = error instanceof Error ? error.message : String(error);
226
- throw new Error(`Failed to prepare Gemini image attachment: ${message}`);
227
- }
228
- }
229
- const sections = [];
230
- if (textBlocks.length > 0) {
231
- sections.push(textBlocks.join("\n\n"));
232
- }
233
- else if (imageReferences.length > 0) {
234
- sections.push(GEMINI_IMAGE_ONLY_PROMPT);
235
- }
236
- if (imageReferences.length > 0) {
237
- sections.push([
238
- imageReferences.length === 1
239
- ? "Attached image file:"
240
- : "Attached image files:",
241
- ...imageReferences,
242
- ].join("\n"));
243
- }
244
- return sections.join("\n\n");
245
- }
246
- function mapGeminiUsage(stats) {
247
- if (!stats) {
248
- return undefined;
249
- }
253
+ export function getRequestedGeminiSessionState(config) {
250
254
  return {
251
- inputTokens: stats.input_tokens ?? stats.input,
252
- cachedInputTokens: stats.cached,
253
- outputTokens: stats.output_tokens,
255
+ modeId: config.modeId ? normalizeGeminiMode(config.modeId) : null,
256
+ modelId: typeof config.model === "string" && config.model.trim().length > 0
257
+ ? config.model.trim()
258
+ : null,
254
259
  };
255
260
  }
261
+ export function toGeminiPromptBlocks(prompt) {
262
+ if (typeof prompt === "string") {
263
+ return [{ type: "text", text: prompt }];
264
+ }
265
+ return prompt.map((block) => block.type === "text"
266
+ ? { type: "text", text: block.text }
267
+ : {
268
+ type: "image",
269
+ data: block.data,
270
+ mimeType: block.mimeType,
271
+ });
272
+ }
256
273
  function normalizeToolStatus(status) {
257
274
  const normalized = status?.trim().toLowerCase() ?? "";
258
- if (normalized === "success" || normalized === "completed" || normalized === "done") {
275
+ if (normalized === "completed" ||
276
+ normalized === "success" ||
277
+ normalized === "done") {
259
278
  return "completed";
260
279
  }
261
- if (normalized === "error" ||
262
- normalized === "failed" ||
280
+ if (normalized === "failed" ||
281
+ normalized === "error" ||
263
282
  normalized === "failure") {
264
283
  return "failed";
265
284
  }
@@ -271,59 +290,122 @@ function normalizeToolStatus(status) {
271
290
  }
272
291
  return "running";
273
292
  }
274
- function toPlainTextToolDetail(toolName, output) {
275
- const text = nonEmptyString(typeof output === "string" ? output : undefined) ??
276
- (typeof output === "object" && output !== null && "message" in output
277
- ? nonEmptyString(output.message)
278
- : undefined);
279
- return {
280
- type: "plain_text",
281
- label: toolName,
282
- ...(text ? { text } : {}),
283
- };
293
+ function toolCallContentToText(content) {
294
+ if (!content || content.length === 0) {
295
+ return null;
296
+ }
297
+ const segments = [];
298
+ for (const part of content) {
299
+ if (part.type === "content" && part.content.type === "text") {
300
+ const text = part.content.text.trim();
301
+ if (text.length > 0) {
302
+ segments.push(text);
303
+ }
304
+ }
305
+ }
306
+ if (segments.length === 0) {
307
+ return null;
308
+ }
309
+ return segments.join("\n\n");
284
310
  }
285
- function deriveGeminiToolDetail(toolName, input, output) {
286
- const normalized = toolName.trim().toLowerCase();
287
- if (normalized === "run_shell_command" ||
288
- normalized === "shell" ||
289
- normalized === "bash" ||
290
- normalized === "exec_command") {
311
+ function detailFromAcpDiffContent(content) {
312
+ if (!content) {
313
+ return null;
314
+ }
315
+ for (const part of content) {
316
+ if (part.type !== "diff") {
317
+ continue;
318
+ }
319
+ return {
320
+ type: "edit",
321
+ filePath: part.path,
322
+ ...(typeof part.oldText === "string" ? { oldString: part.oldText } : {}),
323
+ newString: part.newText,
324
+ };
325
+ }
326
+ return null;
327
+ }
328
+ function deriveGeminiToolDetail(toolName, input, output, options) {
329
+ const fromDiff = detailFromAcpDiffContent(options?.content);
330
+ if (fromDiff) {
331
+ return fromDiff;
332
+ }
333
+ const normalizedKind = options?.kind?.trim().toLowerCase() ?? "";
334
+ const normalizedName = toolName.trim().toLowerCase();
335
+ const contentText = toolCallContentToText(options?.content);
336
+ const locationPath = options?.locations?.[0]?.path;
337
+ if (normalizedKind === "execute" ||
338
+ normalizedName === "run_shell_command" ||
339
+ normalizedName === "shell" ||
340
+ normalizedName === "bash" ||
341
+ normalizedName === "exec_command") {
291
342
  const parsedInput = ToolShellInputSchema.safeParse(input);
292
343
  const parsedOutput = ToolShellOutputSchema.safeParse(output);
293
- return (toShellToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? toPlainTextToolDetail(toolName, output));
344
+ return (toShellToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
345
+ type: "shell",
346
+ command: toolName,
347
+ ...(contentText ? { output: contentText } : {}),
348
+ });
294
349
  }
295
- if (normalized === "read_file" ||
296
- normalized === "read" ||
297
- normalized === "view") {
350
+ if (normalizedKind === "read" ||
351
+ normalizedName === "read_file" ||
352
+ normalizedName === "read" ||
353
+ normalizedName === "view") {
298
354
  const parsedInput = ToolReadInputSchema.safeParse(input);
299
355
  const parsedOutput = ToolReadOutputSchema.safeParse(output);
300
- return (toReadToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? toPlainTextToolDetail(toolName, output));
356
+ return (toReadToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
357
+ type: "read",
358
+ filePath: locationPath ?? toolName,
359
+ ...(contentText ? { content: contentText } : {}),
360
+ });
301
361
  }
302
- if (normalized === "write_file" ||
303
- normalized === "write" ||
304
- normalized === "create_file") {
362
+ if (normalizedKind === "edit" ||
363
+ normalizedKind === "delete" ||
364
+ normalizedKind === "move" ||
365
+ normalizedName === "replace" ||
366
+ normalizedName === "edit" ||
367
+ normalizedName === "apply_patch" ||
368
+ normalizedName === "apply_diff") {
369
+ const parsedInput = ToolEditInputSchema.safeParse(input);
370
+ const parsedOutput = ToolEditOutputSchema.safeParse(output);
371
+ return (toEditToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
372
+ type: "edit",
373
+ filePath: locationPath ?? toolName,
374
+ ...(contentText ? { unifiedDiff: contentText } : {}),
375
+ });
376
+ }
377
+ if (normalizedName === "write_file" ||
378
+ normalizedName === "write" ||
379
+ normalizedName === "create_file") {
305
380
  const parsedInput = ToolWriteInputSchema.safeParse(input);
306
381
  const parsedOutput = ToolWriteOutputSchema.safeParse(output);
307
- return (toWriteToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? toPlainTextToolDetail(toolName, output));
382
+ return (toWriteToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? {
383
+ type: "write",
384
+ filePath: locationPath ?? toolName,
385
+ ...(contentText ? { content: contentText } : {}),
386
+ });
308
387
  }
309
- if (normalized === "replace" ||
310
- normalized === "edit" ||
311
- normalized === "apply_patch" ||
312
- normalized === "apply_diff") {
313
- const parsedInput = ToolEditInputSchema.safeParse(input);
314
- const parsedOutput = ToolEditOutputSchema.safeParse(output);
315
- return (toEditToolDetail(parsedInput.success ? parsedInput.data : null, parsedOutput.success ? parsedOutput.data : null) ?? toPlainTextToolDetail(toolName, output));
316
- }
317
- if (normalized === "grep" ||
318
- normalized === "glob" ||
319
- normalized === "search_text" ||
320
- normalized === "search" ||
321
- normalized === "web_search" ||
322
- normalized === "google_search" ||
323
- normalized === "web_fetch") {
388
+ if (normalizedKind === "search" ||
389
+ normalizedKind === "fetch" ||
390
+ normalizedName === "grep" ||
391
+ normalizedName === "glob" ||
392
+ normalizedName === "search_text" ||
393
+ normalizedName === "search" ||
394
+ normalizedName === "web_search" ||
395
+ normalizedName === "google_search" ||
396
+ normalizedName === "web_fetch") {
324
397
  const parsedInput = ToolSearchInputSchema.safeParse(input);
325
- return (toSearchToolDetail(parsedInput.success ? parsedInput.data : null) ??
326
- toPlainTextToolDetail(toolName, output));
398
+ return (toSearchToolDetail(parsedInput.success ? parsedInput.data : null) ?? {
399
+ type: "search",
400
+ query: toolName,
401
+ });
402
+ }
403
+ if (contentText) {
404
+ return {
405
+ type: "plain_text",
406
+ label: toolName,
407
+ text: contentText,
408
+ };
327
409
  }
328
410
  return {
329
411
  type: "unknown",
@@ -339,7 +421,11 @@ export function toGeminiToolTimelineItem(params) {
339
421
  input: params.input,
340
422
  });
341
423
  const status = normalizeToolStatus(params.status);
342
- const detail = deriveGeminiToolDetail(params.toolName, params.input ?? null, params.output ?? null);
424
+ const detail = deriveGeminiToolDetail(params.toolName, params.input ?? null, params.output ?? null, {
425
+ kind: params.kind,
426
+ content: params.content ?? null,
427
+ locations: params.locations ?? null,
428
+ });
343
429
  if (status === "failed") {
344
430
  return {
345
431
  type: "tool_call",
@@ -348,6 +434,9 @@ export function toGeminiToolTimelineItem(params) {
348
434
  status,
349
435
  detail,
350
436
  error: params.error ?? { message: "Tool call failed" },
437
+ metadata: {
438
+ ...(params.kind ? { kind: params.kind } : {}),
439
+ },
351
440
  };
352
441
  }
353
442
  return {
@@ -357,6 +446,9 @@ export function toGeminiToolTimelineItem(params) {
357
446
  status,
358
447
  detail,
359
448
  error: null,
449
+ metadata: {
450
+ ...(params.kind ? { kind: params.kind } : {}),
451
+ },
360
452
  };
361
453
  }
362
454
  function toGeminiModelDefinitions() {
@@ -365,66 +457,9 @@ function toGeminiModelDefinitions() {
365
457
  id: model.id,
366
458
  label: model.label,
367
459
  description: model.description,
368
- ...("isDefault" in model && model.isDefault === true ? { isDefault: true } : {}),
460
+ ...("isDefault" in model && model.isDefault ? { isDefault: true } : {}),
369
461
  }));
370
462
  }
371
- async function resolveProjectTempDirForCwd(cwd) {
372
- const normalizedCwd = await normalizeComparablePath(cwd);
373
- let entries;
374
- try {
375
- entries = await readdir(GEMINI_TMP_DIR);
376
- }
377
- catch {
378
- return null;
379
- }
380
- for (const entry of entries) {
381
- const candidateDir = path.join(GEMINI_TMP_DIR, entry);
382
- const markerPath = path.join(candidateDir, ".project_root");
383
- try {
384
- const marker = (await readFile(markerPath, "utf8")).trim();
385
- const normalizedMarker = await normalizeComparablePath(marker);
386
- if (normalizedMarker === normalizedCwd) {
387
- return candidateDir;
388
- }
389
- }
390
- catch {
391
- // ignore non-project entries
392
- }
393
- }
394
- return null;
395
- }
396
- async function listKnownGeminiProjects() {
397
- let entries;
398
- try {
399
- entries = await readdir(GEMINI_TMP_DIR);
400
- }
401
- catch {
402
- return [];
403
- }
404
- const projects = [];
405
- for (const entry of entries) {
406
- const tempDir = path.join(GEMINI_TMP_DIR, entry);
407
- const markerPath = path.join(tempDir, ".project_root");
408
- try {
409
- const cwd = (await readFile(markerPath, "utf8")).trim();
410
- if (cwd.length > 0) {
411
- projects.push({ cwd, tempDir });
412
- }
413
- }
414
- catch {
415
- // ignore
416
- }
417
- }
418
- return projects;
419
- }
420
- async function normalizeComparablePath(inputPath) {
421
- try {
422
- return await realpath(inputPath);
423
- }
424
- catch {
425
- return path.resolve(inputPath);
426
- }
427
- }
428
463
  async function readGeminiSessionFile(filePath) {
429
464
  try {
430
465
  const raw = await readFile(filePath, "utf8");
@@ -479,6 +514,19 @@ function getGeminiSessionTitle(session) {
479
514
  }
480
515
  return null;
481
516
  }
517
+ function extractRecordedThoughtText(thought) {
518
+ const directText = nonEmptyString(thought.text) ?? nonEmptyString(thought.thought);
519
+ if (directText) {
520
+ return directText;
521
+ }
522
+ const record = thought;
523
+ const subject = nonEmptyString(record.subject);
524
+ const description = nonEmptyString(record.description);
525
+ if (subject && description) {
526
+ return `**${subject}**\n${description}`;
527
+ }
528
+ return subject ?? description ?? "";
529
+ }
482
530
  export function buildGeminiHistoryTimeline(session) {
483
531
  const items = [];
484
532
  for (const message of session.messages) {
@@ -501,7 +549,7 @@ export function buildGeminiHistoryTimeline(session) {
501
549
  items.push({ type: "assistant_message", text: assistantText });
502
550
  }
503
551
  for (const thought of message.thoughts ?? []) {
504
- const text = nonEmptyString(thought.text) ?? nonEmptyString(thought.thought);
552
+ const text = extractRecordedThoughtText(thought);
505
553
  if (text) {
506
554
  items.push({ type: "reasoning", text });
507
555
  }
@@ -522,6 +570,58 @@ export function buildGeminiHistoryTimeline(session) {
522
570
  }
523
571
  return items;
524
572
  }
573
+ async function listKnownGeminiProjects() {
574
+ let entries;
575
+ try {
576
+ entries = await readdir(GEMINI_TMP_DIR);
577
+ }
578
+ catch {
579
+ return [];
580
+ }
581
+ const projects = [];
582
+ for (const entry of entries) {
583
+ const tempDir = path.join(GEMINI_TMP_DIR, entry);
584
+ const markerPath = path.join(tempDir, ".project_root");
585
+ try {
586
+ const cwd = (await readFile(markerPath, "utf8")).trim();
587
+ if (cwd.length > 0) {
588
+ projects.push({ cwd, tempDir });
589
+ }
590
+ }
591
+ catch {
592
+ // ignore
593
+ }
594
+ }
595
+ return projects;
596
+ }
597
+ async function resolveGeminiProjectTempDirForCwd(cwd) {
598
+ const projects = await listKnownGeminiProjects();
599
+ return projects.find((project) => project.cwd === cwd)?.tempDir ?? null;
600
+ }
601
+ async function loadGeminiRecordedSessionForCwd(cwd, sessionId) {
602
+ const tempDir = await resolveGeminiProjectTempDirForCwd(cwd);
603
+ if (!tempDir) {
604
+ return null;
605
+ }
606
+ const chatsDir = path.join(tempDir, "chats");
607
+ let chatFiles;
608
+ try {
609
+ chatFiles = await readdir(chatsDir);
610
+ }
611
+ catch {
612
+ return null;
613
+ }
614
+ for (const fileName of chatFiles) {
615
+ if (!fileName.startsWith("session-") || !fileName.endsWith(".json")) {
616
+ continue;
617
+ }
618
+ const session = await readGeminiSessionFile(path.join(chatsDir, fileName));
619
+ if (session?.sessionId === sessionId) {
620
+ return session;
621
+ }
622
+ }
623
+ return null;
624
+ }
525
625
  async function listGeminiSessionsForProject(params) {
526
626
  const chatsDir = path.join(params.tempDir, "chats");
527
627
  let chatFiles;
@@ -560,41 +660,281 @@ async function listGeminiSessionsForProject(params) {
560
660
  });
561
661
  }
562
662
  return sessions
563
- .sort((a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime())
663
+ .sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
564
664
  .slice(0, params.limit ?? sessions.length);
565
665
  }
566
- function toGeminiMcpServerConfig(config) {
666
+ function toAcpHeaders(headers) {
667
+ if (!headers) {
668
+ return [];
669
+ }
670
+ return Object.entries(headers).map(([name, value]) => ({ name, value }));
671
+ }
672
+ function toAcpEnvVars(env) {
673
+ if (!env) {
674
+ return [];
675
+ }
676
+ return Object.entries(env).map(([name, value]) => ({ name, value }));
677
+ }
678
+ function toGeminiAcpMcpServer(name, config) {
567
679
  if (config.type === "stdio") {
568
680
  return {
681
+ name,
569
682
  command: config.command,
570
- ...(config.args ? { args: config.args } : {}),
571
- ...(config.env ? { env: config.env } : {}),
683
+ args: config.args ?? [],
684
+ env: toAcpEnvVars(config.env),
572
685
  };
573
686
  }
574
687
  return {
575
688
  type: config.type,
689
+ name,
576
690
  url: config.url,
577
- ...(config.headers ? { headers: config.headers } : {}),
691
+ headers: toAcpHeaders(config.headers),
578
692
  };
579
693
  }
580
- async function writeGeminiOverlayConfig(modeId, mcpServers) {
581
- const overlay = {};
582
- if (modeId === "plan") {
583
- overlay.experimental = { plan: true };
694
+ function toGeminiAcpMcpServers(mcpServers) {
695
+ if (!mcpServers) {
696
+ return [];
584
697
  }
585
- if (mcpServers && Object.keys(mcpServers).length > 0) {
586
- overlay.mcpServers = Object.fromEntries(Object.entries(mcpServers).map(([name, serverConfig]) => [
587
- name,
588
- toGeminiMcpServerConfig(serverConfig),
589
- ]));
698
+ return Object.entries(mcpServers).map(([name, config]) => toGeminiAcpMcpServer(name, config));
699
+ }
700
+ function toAgentMode(mode) {
701
+ return {
702
+ id: mode.id,
703
+ label: mode.name,
704
+ ...(nonEmptyString(mode.description) ? { description: mode.description } : {}),
705
+ };
706
+ }
707
+ function toAgentModelDefinition(model) {
708
+ return {
709
+ provider: GEMINI_PROVIDER,
710
+ id: model.modelId,
711
+ label: model.name,
712
+ ...(nonEmptyString(model.description) ? { description: model.description } : {}),
713
+ };
714
+ }
715
+ function toolCallTitleToInteractionSubtype(title) {
716
+ const normalized = title?.trim() ?? "";
717
+ if (normalized.startsWith("Asking user:")) {
718
+ return "ask_user_unsupported";
590
719
  }
591
- if (Object.keys(overlay).length === 0) {
720
+ if (normalized.startsWith("Requesting plan approval for:")) {
721
+ return "exit_plan_mode_unsupported";
722
+ }
723
+ return "tool";
724
+ }
725
+ function extractGeminiPlanPath(title) {
726
+ const normalized = title?.trim() ?? "";
727
+ const prefix = "Requesting plan approval for:";
728
+ if (!normalized.startsWith(prefix)) {
592
729
  return null;
593
730
  }
594
- const dir = await mkdtemp(path.join(tmpdir(), "junction-gemini-settings-"));
595
- const settingsPath = path.join(dir, "settings.json");
596
- await writeFile(settingsPath, JSON.stringify(overlay, null, 2), "utf8");
597
- return { dir, settingsPath };
731
+ return nonEmptyString(normalized.slice(prefix.length).trim()) ?? null;
732
+ }
733
+ export function classifyGeminiPermissionRequest(params) {
734
+ return toolCallTitleToInteractionSubtype(params.toolCall.title);
735
+ }
736
+ export function toGeminiPermissionRequest(params) {
737
+ const interactionSubtype = classifyGeminiPermissionRequest(params);
738
+ const toolCallTitle = nonEmptyString(params.toolCall.title) ?? params.toolCall.toolCallId;
739
+ const optionMetadata = params.options.map((option) => ({
740
+ optionId: option.optionId,
741
+ name: option.name,
742
+ kind: option.kind,
743
+ }));
744
+ const planPath = extractGeminiPlanPath(toolCallTitle);
745
+ const detail = toGeminiToolTimelineItem({
746
+ toolName: toolCallTitle,
747
+ callId: params.toolCall.toolCallId,
748
+ status: params.toolCall.status,
749
+ input: params.toolCall.rawInput,
750
+ output: params.toolCall.rawOutput,
751
+ kind: params.toolCall.kind,
752
+ content: params.toolCall.content ?? null,
753
+ locations: params.toolCall.locations ?? null,
754
+ }).detail;
755
+ const metadata = {
756
+ geminiInteractionSubtype: interactionSubtype,
757
+ permissionOptions: optionMetadata,
758
+ toolKind: params.toolCall.kind ?? null,
759
+ toolCallId: params.toolCall.toolCallId,
760
+ toolCallTitle,
761
+ toolCallContent: params.toolCall.content ?? [],
762
+ toolCallLocations: params.toolCall.locations ?? [],
763
+ ...(planPath ? { geminiPlanPath: planPath } : {}),
764
+ };
765
+ if (params.toolCall.rawInput !== undefined) {
766
+ metadata.toolCallRawInput = params.toolCall.rawInput;
767
+ }
768
+ if (params.toolCall.rawOutput !== undefined) {
769
+ metadata.toolCallRawOutput = params.toolCall.rawOutput;
770
+ }
771
+ if (interactionSubtype === "ask_user_unsupported") {
772
+ return {
773
+ id: params.toolCall.toolCallId,
774
+ provider: GEMINI_PROVIDER,
775
+ name: "ask_user",
776
+ kind: "other",
777
+ title: "Gemini needs answers from ask_user",
778
+ description: "Gemini ACP does not include the payload needed to answer ask_user prompts from Junction.",
779
+ detail,
780
+ metadata,
781
+ };
782
+ }
783
+ if (interactionSubtype === "exit_plan_mode_unsupported") {
784
+ return {
785
+ id: params.toolCall.toolCallId,
786
+ provider: GEMINI_PROVIDER,
787
+ name: "exit_plan_mode",
788
+ kind: "other",
789
+ title: "Gemini requested plan approval",
790
+ description: "Gemini ACP does not include the payload needed to approve or revise exit_plan_mode from Junction.",
791
+ detail,
792
+ metadata,
793
+ };
794
+ }
795
+ return {
796
+ id: params.toolCall.toolCallId,
797
+ provider: GEMINI_PROVIDER,
798
+ name: toolCallTitle,
799
+ kind: "tool",
800
+ title: toolCallTitle,
801
+ description: "Gemini needs approval before continuing.",
802
+ detail,
803
+ metadata,
804
+ };
805
+ }
806
+ function toolStateToTimelineItem(state) {
807
+ return toGeminiToolTimelineItem({
808
+ toolName: state.title,
809
+ callId: state.toolCallId,
810
+ status: state.status,
811
+ input: state.rawInput,
812
+ output: state.rawOutput,
813
+ kind: state.kind,
814
+ content: state.content,
815
+ locations: state.locations,
816
+ error: normalizeToolStatus(state.status) === "failed"
817
+ ? state.rawOutput ?? { message: "Tool call failed" }
818
+ : null,
819
+ });
820
+ }
821
+ function sessionUpdateToEvents(update, toolStates, options) {
822
+ switch (update.sessionUpdate) {
823
+ case "user_message_chunk": {
824
+ if (!options?.includeUserMessages || update.content.type !== "text") {
825
+ return [];
826
+ }
827
+ const text = update.content.text.trim();
828
+ if (text.length === 0) {
829
+ return [];
830
+ }
831
+ return [
832
+ {
833
+ type: "timeline",
834
+ provider: GEMINI_PROVIDER,
835
+ item: {
836
+ type: "user_message",
837
+ text,
838
+ },
839
+ },
840
+ ];
841
+ }
842
+ case "agent_message_chunk": {
843
+ if (update.content.type !== "text") {
844
+ return [];
845
+ }
846
+ const text = update.content.text;
847
+ if (text.length === 0) {
848
+ return [];
849
+ }
850
+ return [
851
+ {
852
+ type: "timeline",
853
+ provider: GEMINI_PROVIDER,
854
+ item: {
855
+ type: "assistant_message",
856
+ text,
857
+ },
858
+ },
859
+ ];
860
+ }
861
+ case "agent_thought_chunk": {
862
+ if (update.content.type !== "text") {
863
+ return [];
864
+ }
865
+ const text = update.content.text;
866
+ if (text.length === 0) {
867
+ return [];
868
+ }
869
+ return [
870
+ {
871
+ type: "timeline",
872
+ provider: GEMINI_PROVIDER,
873
+ item: {
874
+ type: "reasoning",
875
+ text,
876
+ },
877
+ },
878
+ ];
879
+ }
880
+ case "tool_call": {
881
+ const state = {
882
+ toolCallId: update.toolCallId,
883
+ title: update.title,
884
+ status: update.status ?? "in_progress",
885
+ kind: update.kind ?? null,
886
+ content: update.content ?? null,
887
+ locations: update.locations ?? null,
888
+ rawInput: update.rawInput,
889
+ rawOutput: update.rawOutput,
890
+ };
891
+ toolStates.set(update.toolCallId, state);
892
+ return [
893
+ {
894
+ type: "timeline",
895
+ provider: GEMINI_PROVIDER,
896
+ item: toolStateToTimelineItem(state),
897
+ },
898
+ ];
899
+ }
900
+ case "tool_call_update": {
901
+ const previous = toolStates.get(update.toolCallId);
902
+ const state = {
903
+ toolCallId: update.toolCallId,
904
+ title: nonEmptyString(update.title) ?? previous?.title ?? update.toolCallId,
905
+ status: update.status ?? previous?.status ?? "in_progress",
906
+ kind: update.kind ?? previous?.kind ?? null,
907
+ content: update.content ?? previous?.content ?? null,
908
+ locations: update.locations ?? previous?.locations ?? null,
909
+ rawInput: update.rawInput !== undefined ? update.rawInput : previous?.rawInput,
910
+ rawOutput: update.rawOutput !== undefined ? update.rawOutput : previous?.rawOutput,
911
+ };
912
+ toolStates.set(update.toolCallId, state);
913
+ return [
914
+ {
915
+ type: "timeline",
916
+ provider: GEMINI_PROVIDER,
917
+ item: toolStateToTimelineItem(state),
918
+ },
919
+ ];
920
+ }
921
+ case "plan":
922
+ return [
923
+ {
924
+ type: "timeline",
925
+ provider: GEMINI_PROVIDER,
926
+ item: {
927
+ type: "todo",
928
+ items: update.entries.map((entry) => ({
929
+ text: entry.content,
930
+ completed: entry.status === "completed",
931
+ })),
932
+ },
933
+ },
934
+ ];
935
+ default:
936
+ return [];
937
+ }
598
938
  }
599
939
  export class GeminiAgentClient {
600
940
  constructor(logger, runtimeSettings) {
@@ -605,7 +945,9 @@ export class GeminiAgentClient {
605
945
  }
606
946
  async createSession(config) {
607
947
  const geminiConfig = this.assertConfig(config);
608
- return new GeminiAgentSession(geminiConfig, this.logger, this.runtimeSettings);
948
+ const session = new GeminiAgentSession(geminiConfig, this.logger, this.runtimeSettings);
949
+ await session.initialize();
950
+ return session;
609
951
  }
610
952
  async resumeSession(handle, overrides) {
611
953
  const cwd = overrides?.cwd ?? handle.metadata?.cwd;
@@ -617,7 +959,9 @@ export class GeminiAgentClient {
617
959
  cwd,
618
960
  ...overrides,
619
961
  });
620
- return new GeminiAgentSession(config, this.logger, this.runtimeSettings, handle.sessionId);
962
+ const session = new GeminiAgentSession(config, this.logger, this.runtimeSettings, handle.sessionId);
963
+ await session.initialize();
964
+ return session;
621
965
  }
622
966
  async listModels(_options) {
623
967
  return toGeminiModelDefinitions();
@@ -630,11 +974,21 @@ export class GeminiAgentClient {
630
974
  })));
631
975
  return sessions
632
976
  .flat()
633
- .sort((a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime())
977
+ .sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
634
978
  .slice(0, options?.limit ?? 20);
635
979
  }
636
980
  async isAvailable() {
637
- return isProviderCommandAvailable(this.runtimeSettings?.command, resolveGeminiBinary);
981
+ try {
982
+ const available = isProviderCommandAvailable(this.runtimeSettings?.command, resolveGeminiBinary, applyProviderEnv(process.env, this.runtimeSettings));
983
+ if (!available) {
984
+ return false;
985
+ }
986
+ assertGeminiAcpSupport(this.runtimeSettings);
987
+ return true;
988
+ }
989
+ catch {
990
+ return false;
991
+ }
638
992
  }
639
993
  assertConfig(config) {
640
994
  if (config.provider !== GEMINI_PROVIDER) {
@@ -647,24 +1001,53 @@ class GeminiAgentSession {
647
1001
  constructor(config, logger, runtimeSettings, sessionId) {
648
1002
  this.provider = GEMINI_PROVIDER;
649
1003
  this.capabilities = GEMINI_CAPABILITIES;
650
- this.currentChild = null;
651
- this.currentRunInterrupted = false;
652
- this.overlayDir = null;
1004
+ this.connection = null;
1005
+ this.child = null;
1006
+ this.closePromise = null;
1007
+ this.initialized = false;
1008
+ this.closed = false;
1009
+ this.currentModeId = null;
1010
+ this.currentModelId = null;
1011
+ this.requestedModeId = null;
1012
+ this.requestedModelId = null;
1013
+ this.availableModes = GEMINI_RUNTIME_MODES;
1014
+ this.availableModels = toGeminiModelDefinitions();
653
1015
  this.pendingPermissions = new Map();
1016
+ this.toolStates = new Map();
1017
+ this.activeTurnQueue = null;
1018
+ this.historyEvents = [];
1019
+ this.historyReadyPromise = null;
1020
+ this.resolveHistoryReady = null;
1021
+ this.historyCaptureActive = false;
1022
+ this.historyIdleTimer = null;
1023
+ this.expectedHistoryEventCount = null;
1024
+ this.capturedHistoryEventCount = 0;
1025
+ this.currentRunInterrupted = false;
654
1026
  this.config = config;
655
1027
  this.logger = logger.child({ sessionProvider: GEMINI_PROVIDER });
656
1028
  this.runtimeSettings = runtimeSettings;
657
- this.currentMode = normalizeGeminiMode(config.modeId);
658
1029
  this.sessionId = sessionId ?? null;
1030
+ const requestedState = getRequestedGeminiSessionState(config);
1031
+ this.requestedModeId = requestedState.modeId;
1032
+ this.requestedModelId = requestedState.modelId;
1033
+ this.currentModeId = requestedState.modeId;
1034
+ this.currentModelId = requestedState.modelId;
659
1035
  this.config.thinkingOptionId = undefined;
660
1036
  }
661
1037
  get id() {
662
1038
  return this.sessionId;
663
1039
  }
1040
+ async initialize() {
1041
+ if (this.initialized) {
1042
+ return;
1043
+ }
1044
+ assertGeminiAcpSupport(this.runtimeSettings);
1045
+ await this.openConnection();
1046
+ this.initialized = true;
1047
+ }
664
1048
  async run(prompt, options) {
665
1049
  const timeline = [];
666
1050
  let finalText = "";
667
- let usage;
668
1051
  let canceled = false;
669
1052
  for await (const event of this.stream(prompt, options)) {
670
1053
  if (event.type === "timeline") {
@@ -673,9 +1056,6 @@ class GeminiAgentSession {
673
1056
  finalText += event.item.text;
674
1057
  }
675
1058
  }
676
- else if (event.type === "turn_completed") {
677
- usage = event.usage;
678
- }
679
1059
  else if (event.type === "turn_failed") {
680
1060
  throw new Error(event.error);
681
1061
  }
@@ -686,249 +1066,156 @@ class GeminiAgentSession {
686
1066
  return {
687
1067
  sessionId: this.sessionId ?? "",
688
1068
  finalText,
689
- usage,
690
1069
  timeline,
691
1070
  ...(canceled ? { canceled } : {}),
692
1071
  };
693
1072
  }
694
1073
  async *stream(prompt, _options) {
695
- if (this.currentChild) {
696
- throw new Error("Gemini session is already running");
1074
+ await this.initialize();
1075
+ if (!this.connection || !this.sessionId) {
1076
+ throw new Error("Gemini ACP session is not ready");
697
1077
  }
698
- const promptText = await normalizeGeminiPromptInput(prompt);
699
- const modeId = this.currentMode;
700
- const overlay = await writeGeminiOverlayConfig(modeId, this.config.mcpServers);
701
- if (overlay) {
702
- await this.cleanupOverlay();
703
- this.overlayDir = overlay.dir;
1078
+ if (this.activeTurnQueue) {
1079
+ throw new Error("Gemini session is already running");
704
1080
  }
705
- const launchPrefix = resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveGeminiBinary);
706
- const env = applyProviderEnv(process.env, this.runtimeSettings);
707
- if (overlay) {
708
- env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = overlay.settingsPath;
709
- }
710
- const args = [
711
- ...launchPrefix.args,
712
- "-p",
713
- promptText,
714
- "--output-format",
715
- "stream-json",
716
- "--approval-mode",
717
- modeId,
718
- ];
719
- const modelId = typeof this.config.model === "string" && this.config.model.trim().length > 0
720
- ? this.config.model.trim()
721
- : null;
722
- if (modelId) {
723
- args.push("--model", modelId);
724
- }
725
- if (this.sessionId) {
726
- args.push("--resume", this.sessionId);
727
- }
728
- const child = spawn(launchPrefix.command, args, {
729
- cwd: this.config.cwd,
730
- env,
731
- stdio: ["ignore", "pipe", "pipe"],
732
- });
733
- this.currentChild = child;
1081
+ const queue = new AsyncEventQueue();
1082
+ this.activeTurnQueue = queue;
734
1083
  this.currentRunInterrupted = false;
735
- const stderrChunks = [];
736
- child.stderr.on("data", (chunk) => {
737
- const text = chunk.toString("utf8");
738
- if (text.length > 0) {
739
- stderrChunks.push(text);
740
- }
741
- });
742
- const closePromise = new Promise((resolve, reject) => {
743
- child.once("error", reject);
744
- child.once("close", (code, signal) => resolve({ code, signal }));
745
- });
746
- const lineReader = readline.createInterface({
747
- input: child.stdout,
748
- crlfDelay: Infinity,
749
- });
750
- const pendingToolCalls = new Map();
751
- let sawResult = false;
752
- let failureMessage = null;
753
- yield {
1084
+ queue.push({
754
1085
  type: "turn_started",
755
1086
  provider: GEMINI_PROVIDER,
756
- };
1087
+ });
1088
+ void this.connection
1089
+ .prompt({
1090
+ sessionId: this.sessionId,
1091
+ prompt: toGeminiPromptBlocks(prompt),
1092
+ })
1093
+ .then((response) => {
1094
+ if (response.stopReason === "cancelled") {
1095
+ queue.push({
1096
+ type: "turn_canceled",
1097
+ provider: GEMINI_PROVIDER,
1098
+ reason: "Interrupted",
1099
+ });
1100
+ }
1101
+ else {
1102
+ queue.push({
1103
+ type: "turn_completed",
1104
+ provider: GEMINI_PROVIDER,
1105
+ });
1106
+ }
1107
+ queue.close();
1108
+ })
1109
+ .catch((error) => {
1110
+ queue.push({
1111
+ type: "turn_failed",
1112
+ provider: GEMINI_PROVIDER,
1113
+ error: formatGeminiAcpError(error),
1114
+ });
1115
+ queue.close();
1116
+ });
757
1117
  try {
758
- for await (const rawLine of lineReader) {
759
- const line = rawLine.trim();
760
- if (!line) {
761
- continue;
762
- }
763
- let parsedJson;
764
- try {
765
- parsedJson = JSON.parse(line);
766
- }
767
- catch {
768
- continue;
769
- }
770
- const parsedEvent = GeminiStreamEventSchema.safeParse(parsedJson);
771
- if (!parsedEvent.success) {
772
- continue;
773
- }
774
- const event = parsedEvent.data;
775
- if (event.type === "init") {
776
- this.sessionId = event.session_id;
777
- if (event.model) {
778
- this.config.model = event.model;
779
- }
780
- yield {
781
- type: "thread_started",
782
- provider: GEMINI_PROVIDER,
783
- sessionId: event.session_id,
784
- };
785
- continue;
786
- }
787
- if (event.type === "message") {
788
- if (event.role === "assistant" && event.content.length > 0) {
789
- yield {
790
- type: "timeline",
791
- provider: GEMINI_PROVIDER,
792
- item: {
793
- type: "assistant_message",
794
- text: event.content,
795
- },
796
- };
797
- }
798
- continue;
799
- }
800
- if (event.type === "tool_use") {
801
- pendingToolCalls.set(event.tool_id, {
802
- toolName: event.tool_name,
803
- input: event.parameters,
804
- });
805
- yield {
806
- type: "timeline",
807
- provider: GEMINI_PROVIDER,
808
- item: toGeminiToolTimelineItem({
809
- toolName: event.tool_name,
810
- callId: event.tool_id,
811
- status: "running",
812
- input: event.parameters,
813
- }),
814
- };
815
- continue;
816
- }
817
- if (event.type === "tool_result") {
818
- const pending = pendingToolCalls.get(event.tool_id);
819
- const toolName = pending?.toolName ?? "tool";
820
- yield {
821
- type: "timeline",
822
- provider: GEMINI_PROVIDER,
823
- item: toGeminiToolTimelineItem({
824
- toolName,
825
- callId: event.tool_id,
826
- status: event.status,
827
- input: pending?.input,
828
- output: event.output,
829
- error: event.error,
830
- }),
831
- };
832
- pendingToolCalls.delete(event.tool_id);
833
- continue;
834
- }
835
- if (event.type === "error") {
836
- failureMessage = event.message;
837
- continue;
838
- }
839
- if (event.type === "result") {
840
- sawResult = true;
841
- if (event.status !== "success") {
842
- failureMessage = failureMessage ?? `Gemini CLI result status: ${event.status}`;
843
- yield {
844
- type: "turn_failed",
845
- provider: GEMINI_PROVIDER,
846
- error: failureMessage,
847
- };
848
- continue;
849
- }
850
- yield {
851
- type: "turn_completed",
852
- provider: GEMINI_PROVIDER,
853
- usage: mapGeminiUsage(event.stats),
854
- };
1118
+ while (true) {
1119
+ const nextEvent = await queue.shift();
1120
+ if (!nextEvent) {
1121
+ break;
855
1122
  }
1123
+ yield nextEvent;
856
1124
  }
857
1125
  }
858
1126
  finally {
859
- lineReader.close();
860
- const { code, signal } = await closePromise.catch((error) => ({
861
- code: 1,
862
- signal: null,
863
- error,
864
- }));
865
- this.currentChild = null;
866
- if (!sawResult) {
867
- if (this.currentRunInterrupted || signal) {
868
- yield {
869
- type: "turn_canceled",
870
- provider: GEMINI_PROVIDER,
871
- reason: "Interrupted",
872
- };
873
- }
874
- else {
875
- const stderr = stderrChunks.join("").trim();
876
- const message = failureMessage ??
877
- stderr ??
878
- (typeof code === "number" && code !== 0
879
- ? `Gemini CLI exited with code ${code}`
880
- : "Gemini CLI ended without a result event");
881
- yield {
882
- type: "turn_failed",
883
- provider: GEMINI_PROVIDER,
884
- error: message,
885
- };
886
- this.logger.warn({ code, signal, message }, "Gemini run failed");
887
- }
1127
+ if (this.activeTurnQueue === queue) {
1128
+ this.activeTurnQueue = null;
888
1129
  }
889
1130
  this.currentRunInterrupted = false;
890
1131
  }
891
1132
  }
892
1133
  async *streamHistory() {
893
- if (!this.sessionId) {
894
- return;
895
- }
896
- const session = await this.loadRecordedSession(this.sessionId);
897
- if (!session) {
898
- return;
1134
+ await this.initialize();
1135
+ if (this.historyReadyPromise) {
1136
+ await this.historyReadyPromise;
899
1137
  }
900
- for (const item of buildGeminiHistoryTimeline(session)) {
901
- yield {
902
- type: "timeline",
903
- provider: GEMINI_PROVIDER,
904
- item,
905
- };
1138
+ for (const event of this.historyEvents) {
1139
+ yield event;
906
1140
  }
907
1141
  }
908
1142
  async getRuntimeInfo() {
1143
+ await this.initialize();
909
1144
  return {
910
1145
  provider: GEMINI_PROVIDER,
911
1146
  sessionId: this.sessionId,
912
- model: this.config.model ?? null,
1147
+ model: this.currentModelId,
913
1148
  thinkingOptionId: null,
914
- modeId: this.currentMode,
1149
+ modeId: this.currentModeId,
1150
+ extra: {
1151
+ availableModels: this.availableModels.map((model) => ({
1152
+ id: model.id,
1153
+ label: model.label,
1154
+ description: model.description ?? null,
1155
+ })),
1156
+ },
915
1157
  };
916
1158
  }
917
1159
  async getAvailableModes() {
918
- return GEMINI_RUNTIME_MODES;
1160
+ await this.initialize();
1161
+ return this.availableModes;
919
1162
  }
920
1163
  async getCurrentMode() {
921
- return this.currentMode;
1164
+ await this.initialize();
1165
+ return this.currentModeId;
922
1166
  }
923
1167
  async setMode(modeId) {
924
- this.currentMode = normalizeGeminiMode(modeId);
925
- this.config.modeId = this.currentMode;
1168
+ await this.initialize();
1169
+ const normalizedMode = normalizeGeminiMode(modeId);
1170
+ const availableModeIds = new Set(this.availableModes.map((mode) => mode.id));
1171
+ if (!availableModeIds.has(normalizedMode)) {
1172
+ throw new Error(`Gemini mode '${normalizedMode}' is unavailable for this session. Available modes: ${this.availableModes
1173
+ .map((mode) => mode.id)
1174
+ .join(", ")}`);
1175
+ }
1176
+ if (!this.connection || !this.sessionId) {
1177
+ throw new Error("Gemini ACP session is not ready");
1178
+ }
1179
+ await this.connection.setSessionMode({
1180
+ sessionId: this.sessionId,
1181
+ modeId: normalizedMode,
1182
+ });
1183
+ this.requestedModeId = normalizedMode;
1184
+ this.currentModeId = normalizedMode;
1185
+ this.config.modeId = normalizedMode;
926
1186
  }
927
1187
  getPendingPermissions() {
928
- return Array.from(this.pendingPermissions.values());
1188
+ return Array.from(this.pendingPermissions.values(), (entry) => entry.request);
929
1189
  }
930
- async respondToPermission(requestId, _response) {
931
- throw new Error(`Gemini permission request '${requestId}' cannot be resumed through the headless CLI integration`);
1190
+ async respondToPermission(requestId, response) {
1191
+ const pending = this.pendingPermissions.get(requestId);
1192
+ if (!pending) {
1193
+ throw new Error(`No pending Gemini permission request with id '${requestId}'`);
1194
+ }
1195
+ const selectedOptionId = response.behavior === "allow"
1196
+ ? nonEmptyString(response.updatedInput?.optionId)
1197
+ : nonEmptyString(response.updatedInput?.optionId);
1198
+ const option = pending.options.find((entry) => entry.optionId === selectedOptionId) ??
1199
+ pending.options.find((entry) => response.behavior === "allow"
1200
+ ? entry.kind.startsWith("allow")
1201
+ : entry.kind.startsWith("reject")) ??
1202
+ null;
1203
+ if (!option) {
1204
+ throw new Error(`Gemini permission '${requestId}' does not have a matching option`);
1205
+ }
1206
+ this.pendingPermissions.delete(requestId);
1207
+ pending.resolve({
1208
+ outcome: {
1209
+ outcome: "selected",
1210
+ optionId: option.optionId,
1211
+ },
1212
+ });
1213
+ this.emitLiveEvent({
1214
+ type: "permission_resolved",
1215
+ provider: GEMINI_PROVIDER,
1216
+ requestId,
1217
+ resolution: response,
1218
+ });
932
1219
  }
933
1220
  describePersistence() {
934
1221
  if (!this.sessionId) {
@@ -937,65 +1224,336 @@ class GeminiAgentSession {
937
1224
  return {
938
1225
  provider: GEMINI_PROVIDER,
939
1226
  sessionId: this.sessionId,
940
- nativeHandle: this.sessionId,
941
1227
  metadata: {
942
1228
  cwd: this.config.cwd,
943
1229
  },
944
1230
  };
945
1231
  }
946
1232
  async interrupt() {
947
- this.currentRunInterrupted = true;
948
- const child = this.currentChild;
949
- if (!child) {
1233
+ await this.initialize();
1234
+ if (!this.connection || !this.sessionId) {
950
1235
  return;
951
1236
  }
952
- child.kill("SIGINT");
1237
+ this.currentRunInterrupted = true;
1238
+ for (const [requestId, pending] of this.pendingPermissions.entries()) {
1239
+ this.pendingPermissions.delete(requestId);
1240
+ pending.resolve({
1241
+ outcome: {
1242
+ outcome: "cancelled",
1243
+ },
1244
+ });
1245
+ this.emitLiveEvent({
1246
+ type: "permission_resolved",
1247
+ provider: GEMINI_PROVIDER,
1248
+ requestId,
1249
+ resolution: {
1250
+ behavior: "deny",
1251
+ message: "Interrupted",
1252
+ },
1253
+ });
1254
+ }
1255
+ await this.connection.cancel({
1256
+ sessionId: this.sessionId,
1257
+ });
953
1258
  }
954
1259
  async close() {
955
- await this.interrupt();
956
- await this.cleanupOverlay();
1260
+ if (this.closed) {
1261
+ return;
1262
+ }
1263
+ this.closed = true;
1264
+ this.finishHistoryCapture();
1265
+ for (const [requestId, pending] of this.pendingPermissions.entries()) {
1266
+ this.pendingPermissions.delete(requestId);
1267
+ pending.resolve({
1268
+ outcome: {
1269
+ outcome: "cancelled",
1270
+ },
1271
+ });
1272
+ this.emitLiveEvent({
1273
+ type: "permission_resolved",
1274
+ provider: GEMINI_PROVIDER,
1275
+ requestId,
1276
+ resolution: {
1277
+ behavior: "deny",
1278
+ message: "Session closed",
1279
+ },
1280
+ });
1281
+ }
1282
+ this.activeTurnQueue?.close();
1283
+ this.activeTurnQueue = null;
1284
+ if (this.child?.stdin) {
1285
+ this.child.stdin.end();
1286
+ }
1287
+ if (this.child && !this.child.killed) {
1288
+ this.child.kill();
1289
+ }
1290
+ await this.closePromise?.catch(() => { });
1291
+ this.connection = null;
1292
+ this.child = null;
957
1293
  }
958
1294
  async setModel(modelId) {
959
- this.config.model =
960
- typeof modelId === "string" && modelId.trim().length > 0
961
- ? modelId.trim()
962
- : undefined;
1295
+ await this.initialize();
1296
+ const normalizedModelId = typeof modelId === "string" && modelId.trim().length > 0 ? modelId.trim() : null;
1297
+ if (!normalizedModelId) {
1298
+ throw new Error("Gemini model id cannot be empty");
1299
+ }
1300
+ const availableModelIds = new Set(this.availableModels.map((model) => model.id));
1301
+ if (!availableModelIds.has(normalizedModelId)) {
1302
+ throw new Error(`Gemini model '${normalizedModelId}' is unavailable for this session. Available models: ${this.availableModels
1303
+ .map((model) => model.id)
1304
+ .join(", ")}`);
1305
+ }
1306
+ if (!this.connection || !this.sessionId) {
1307
+ throw new Error("Gemini ACP session is not ready");
1308
+ }
1309
+ await this.connection.unstable_setSessionModel({
1310
+ sessionId: this.sessionId,
1311
+ modelId: normalizedModelId,
1312
+ });
1313
+ this.requestedModelId = normalizedModelId;
1314
+ this.currentModelId = normalizedModelId;
1315
+ this.config.model = normalizedModelId;
963
1316
  }
964
- async setThinkingOption(_thinkingOptionId) {
965
- this.config.thinkingOptionId = undefined;
1317
+ async setThinkingOption(_thinkingOptionId) { }
1318
+ async openConnection() {
1319
+ const launchPrefix = resolveProviderCommandPrefix(this.runtimeSettings?.command, resolveGeminiBinary);
1320
+ const env = applyProviderEnv(process.env, this.runtimeSettings);
1321
+ const child = spawn(launchPrefix.command, [...launchPrefix.args, "--acp"], {
1322
+ cwd: this.config.cwd,
1323
+ env,
1324
+ stdio: ["pipe", "pipe", "pipe"],
1325
+ });
1326
+ this.child = child;
1327
+ const stderrChunks = [];
1328
+ child.stderr.on("data", (chunk) => {
1329
+ const text = chunk.toString("utf8");
1330
+ if (text.length > 0) {
1331
+ stderrChunks.push(text);
1332
+ }
1333
+ });
1334
+ this.closePromise = new Promise((resolve) => {
1335
+ child.once("close", (code, signal) => {
1336
+ const stderr = stderrChunks.join("").trim();
1337
+ this.child = null;
1338
+ this.connection = null;
1339
+ if (!this.closed) {
1340
+ const message = stderr ||
1341
+ (typeof code === "number" && code !== 0
1342
+ ? `Gemini ACP process exited with code ${code}`
1343
+ : signal
1344
+ ? `Gemini ACP process exited with signal ${signal}`
1345
+ : "Gemini ACP process closed");
1346
+ if (this.activeTurnQueue) {
1347
+ this.activeTurnQueue.push({
1348
+ type: "turn_failed",
1349
+ provider: GEMINI_PROVIDER,
1350
+ error: message,
1351
+ });
1352
+ this.activeTurnQueue.close();
1353
+ }
1354
+ this.logger.warn({ code, signal, stderr }, "Gemini ACP process closed");
1355
+ }
1356
+ resolve();
1357
+ });
1358
+ });
1359
+ const stream = acp.ndJsonStream(NodeWritable.toWeb(child.stdin), NodeReadable.toWeb(child.stdout));
1360
+ this.connection = new acp.ClientSideConnection(() => ({
1361
+ sessionUpdate: async (notification) => {
1362
+ await this.handleSessionUpdate(notification);
1363
+ },
1364
+ requestPermission: async (request) => this.handlePermissionRequest(request),
1365
+ }), stream);
1366
+ try {
1367
+ await this.connection.initialize({
1368
+ protocolVersion: acp.PROTOCOL_VERSION,
1369
+ clientCapabilities: {
1370
+ fs: {
1371
+ readTextFile: false,
1372
+ writeTextFile: false,
1373
+ },
1374
+ },
1375
+ clientInfo: {
1376
+ name: "junction",
1377
+ title: "Junction",
1378
+ version: "local",
1379
+ },
1380
+ });
1381
+ if (this.sessionId) {
1382
+ const recordedSession = await loadGeminiRecordedSessionForCwd(this.config.cwd, this.sessionId);
1383
+ this.beginHistoryCapture(recordedSession ? buildGeminiHistoryTimeline(recordedSession).length : null);
1384
+ const response = await this.connection.loadSession({
1385
+ sessionId: this.sessionId,
1386
+ cwd: this.config.cwd,
1387
+ mcpServers: toGeminiAcpMcpServers(this.config.mcpServers),
1388
+ });
1389
+ this.applySessionSetup(response);
1390
+ if (this.expectedHistoryEventCount === 0) {
1391
+ this.finishHistoryCapture();
1392
+ }
1393
+ else {
1394
+ this.scheduleHistoryCaptureFinish();
1395
+ }
1396
+ }
1397
+ else {
1398
+ const response = await this.connection.newSession({
1399
+ cwd: this.config.cwd,
1400
+ mcpServers: toGeminiAcpMcpServers(this.config.mcpServers),
1401
+ });
1402
+ this.sessionId = response.sessionId;
1403
+ this.applySessionSetup(response);
1404
+ }
1405
+ await this.applyRequestedOverrides();
1406
+ }
1407
+ catch (error) {
1408
+ await this.close().catch(() => { });
1409
+ throw new Error(formatGeminiAcpError(error));
1410
+ }
966
1411
  }
967
- async loadRecordedSession(sessionId) {
968
- const projectTempDir = await resolveProjectTempDirForCwd(this.config.cwd);
969
- if (!projectTempDir) {
970
- return null;
1412
+ applySessionSetup(response) {
1413
+ if (response.modes) {
1414
+ this.availableModes = response.modes.availableModes.map((mode) => toAgentMode(mode));
1415
+ this.currentModeId = response.modes.currentModeId;
1416
+ this.config.modeId = response.modes.currentModeId;
971
1417
  }
972
- const chatsDir = path.join(projectTempDir, "chats");
973
- let files;
974
- try {
975
- files = await readdir(chatsDir);
1418
+ if (response.models) {
1419
+ this.availableModels = response.models.availableModels.map((model) => toAgentModelDefinition(model));
1420
+ this.currentModelId = response.models.currentModelId;
1421
+ this.config.model = response.models.currentModelId;
976
1422
  }
977
- catch {
978
- return null;
1423
+ }
1424
+ async applyRequestedOverrides() {
1425
+ const requestedModeId = this.requestedModeId;
1426
+ if (requestedModeId && requestedModeId !== this.currentModeId) {
1427
+ const availableModeIds = new Set(this.availableModes.map((mode) => mode.id));
1428
+ if (!availableModeIds.has(requestedModeId)) {
1429
+ throw new Error(`Gemini mode '${requestedModeId}' is unavailable for this session. Available modes: ${this.availableModes
1430
+ .map((mode) => mode.id)
1431
+ .join(", ")}`);
1432
+ }
1433
+ if (!this.connection || !this.sessionId) {
1434
+ throw new Error("Gemini ACP session is not ready");
1435
+ }
1436
+ await this.connection.setSessionMode({
1437
+ sessionId: this.sessionId,
1438
+ modeId: requestedModeId,
1439
+ });
1440
+ this.currentModeId = requestedModeId;
1441
+ this.config.modeId = requestedModeId;
979
1442
  }
980
- for (const fileName of files) {
981
- if (!fileName.startsWith("session-") || !fileName.endsWith(".json")) {
982
- continue;
1443
+ const requestedModelId = this.requestedModelId;
1444
+ if (requestedModelId && requestedModelId !== this.currentModelId) {
1445
+ const availableModelIds = new Set(this.availableModels.map((model) => model.id));
1446
+ if (!availableModelIds.has(requestedModelId)) {
1447
+ throw new Error(`Gemini model '${requestedModelId}' is unavailable for this session. Available models: ${this.availableModels
1448
+ .map((model) => model.id)
1449
+ .join(", ")}`);
983
1450
  }
984
- const filePath = path.join(chatsDir, fileName);
985
- const session = await readGeminiSessionFile(filePath);
986
- if (session?.sessionId === sessionId) {
987
- return session;
1451
+ if (!this.connection || !this.sessionId) {
1452
+ throw new Error("Gemini ACP session is not ready");
988
1453
  }
1454
+ await this.connection.unstable_setSessionModel({
1455
+ sessionId: this.sessionId,
1456
+ modelId: requestedModelId,
1457
+ });
1458
+ this.currentModelId = requestedModelId;
1459
+ this.config.model = requestedModelId;
989
1460
  }
990
- return null;
991
1461
  }
992
- async cleanupOverlay() {
993
- if (!this.overlayDir) {
1462
+ beginHistoryCapture(expectedEventCount) {
1463
+ this.historyEvents = [];
1464
+ this.expectedHistoryEventCount = expectedEventCount;
1465
+ this.capturedHistoryEventCount = 0;
1466
+ this.historyCaptureActive = true;
1467
+ this.historyReadyPromise = new Promise((resolve) => {
1468
+ this.resolveHistoryReady = resolve;
1469
+ });
1470
+ }
1471
+ scheduleHistoryCaptureFinish() {
1472
+ if (!this.historyCaptureActive) {
1473
+ return;
1474
+ }
1475
+ if (this.historyIdleTimer) {
1476
+ clearTimeout(this.historyIdleTimer);
1477
+ }
1478
+ this.historyIdleTimer = setTimeout(() => {
1479
+ this.finishHistoryCapture();
1480
+ }, GEMINI_HISTORY_FALLBACK_IDLE_MS);
1481
+ }
1482
+ finishHistoryCapture() {
1483
+ if (this.historyIdleTimer) {
1484
+ clearTimeout(this.historyIdleTimer);
1485
+ this.historyIdleTimer = null;
1486
+ }
1487
+ if (!this.historyCaptureActive) {
1488
+ return;
1489
+ }
1490
+ this.historyCaptureActive = false;
1491
+ this.expectedHistoryEventCount = null;
1492
+ this.capturedHistoryEventCount = 0;
1493
+ this.resolveHistoryReady?.();
1494
+ this.resolveHistoryReady = null;
1495
+ }
1496
+ async handleSessionUpdate(notification) {
1497
+ if (notification.sessionId !== this.sessionId) {
1498
+ return;
1499
+ }
1500
+ if (notification.update.sessionUpdate === "current_mode_update") {
1501
+ this.currentModeId = notification.update.currentModeId;
1502
+ return;
1503
+ }
1504
+ const includeUserMessages = this.historyCaptureActive;
1505
+ const events = sessionUpdateToEvents(notification.update, this.toolStates, {
1506
+ includeUserMessages,
1507
+ });
1508
+ if (events.length === 0) {
994
1509
  return;
995
1510
  }
996
- const dir = this.overlayDir;
997
- this.overlayDir = null;
998
- await rm(dir, { recursive: true, force: true });
1511
+ if (this.historyCaptureActive && !this.activeTurnQueue) {
1512
+ this.historyEvents.push(...events);
1513
+ this.capturedHistoryEventCount += events.length;
1514
+ if (this.expectedHistoryEventCount !== null &&
1515
+ this.capturedHistoryEventCount >= this.expectedHistoryEventCount) {
1516
+ this.finishHistoryCapture();
1517
+ }
1518
+ else {
1519
+ this.scheduleHistoryCaptureFinish();
1520
+ }
1521
+ return;
1522
+ }
1523
+ for (const event of events) {
1524
+ this.emitLiveEvent(event);
1525
+ }
1526
+ }
1527
+ emitLiveEvent(event) {
1528
+ this.activeTurnQueue?.push(event);
1529
+ }
1530
+ async handlePermissionRequest(params) {
1531
+ if (this.currentRunInterrupted) {
1532
+ return {
1533
+ outcome: {
1534
+ outcome: "cancelled",
1535
+ },
1536
+ };
1537
+ }
1538
+ const request = toGeminiPermissionRequest(params);
1539
+ const options = params.options.map((option) => ({
1540
+ optionId: option.optionId,
1541
+ name: option.name,
1542
+ kind: option.kind,
1543
+ }));
1544
+ return await new Promise((resolve, reject) => {
1545
+ this.pendingPermissions.set(request.id, {
1546
+ request,
1547
+ options,
1548
+ resolve,
1549
+ reject,
1550
+ });
1551
+ this.emitLiveEvent({
1552
+ type: "permission_requested",
1553
+ provider: GEMINI_PROVIDER,
1554
+ request,
1555
+ });
1556
+ });
999
1557
  }
1000
1558
  }
1001
1559
  //# sourceMappingURL=gemini-agent.js.map