@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.41

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 (68) hide show
  1. package/README.md +49 -4
  2. package/dist/bin/ai.js +119 -111
  3. package/dist/bin/browser-host.js +265 -0
  4. package/dist/src/agent-mode.js +11 -6
  5. package/dist/src/api/auth.js +6 -4
  6. package/dist/src/api/browser-login.js +152 -37
  7. package/dist/src/api/chat.js +286 -44
  8. package/dist/src/api/contracts.js +55 -1
  9. package/dist/src/api/default-host.js +1 -0
  10. package/dist/src/api/http.js +127 -7
  11. package/dist/src/api/models.js +29 -10
  12. package/dist/src/background-jobs.js +4 -2
  13. package/dist/src/browser/bridge.js +232 -0
  14. package/dist/src/browser/framing.js +42 -0
  15. package/dist/src/browser/native-host.js +209 -0
  16. package/dist/src/browser/protocol.js +46 -0
  17. package/dist/src/browser/session-bridge.js +104 -0
  18. package/dist/src/cli-args.js +19 -5
  19. package/dist/src/client-environment.js +2 -0
  20. package/dist/src/core/clipboard.js +7 -13
  21. package/dist/src/core/image-limits.js +56 -0
  22. package/dist/src/core/image-path-extractor.js +70 -3
  23. package/dist/src/core/session-image-store.js +246 -0
  24. package/dist/src/edit-journal.js +33 -2
  25. package/dist/src/executor.js +25 -3
  26. package/dist/src/help-text.js +58 -17
  27. package/dist/src/patcher.js +43 -4
  28. package/dist/src/permissions.js +288 -0
  29. package/dist/src/session-safety.js +0 -12
  30. package/dist/src/session-store.js +121 -20
  31. package/dist/src/session.js +19 -4
  32. package/dist/src/signin.js +58 -0
  33. package/dist/src/tool-executor.js +12 -46
  34. package/dist/src/tools/browser.js +628 -0
  35. package/dist/src/tools/delete-file.js +20 -4
  36. package/dist/src/tools/index.js +32 -10
  37. package/dist/src/tools/patch-file.js +12 -26
  38. package/dist/src/tools/read-image-file.js +85 -0
  39. package/dist/src/tools/replace-document-text.js +28 -18
  40. package/dist/src/tools/restore-checkpoint.js +0 -1
  41. package/dist/src/tools/run-command.js +14 -71
  42. package/dist/src/tools/run-node-script.js +12 -81
  43. package/dist/src/tools/save-generated-image.js +120 -0
  44. package/dist/src/tools/str-replace.js +12 -26
  45. package/dist/src/tools/undo-edit.js +1 -6
  46. package/dist/src/tools/write-file.js +67 -11
  47. package/dist/src/ui/prompt-history-store.js +1 -1
  48. package/dist/src/ui/repl.js +802 -195
  49. package/dist/src/ui/tui/bridge.js +10 -0
  50. package/dist/src/ui/tui/build-frame.js +517 -165
  51. package/dist/src/ui/tui/composer-layout.js +76 -0
  52. package/dist/src/ui/tui/markdown-render.js +81 -73
  53. package/dist/src/ui/tui/shell-input.js +227 -67
  54. package/dist/src/ui/tui/terminal-theme.js +28 -0
  55. package/dist/src/ui/tui/terminal-title.js +3 -0
  56. package/dist/src/ui/tui/terminal-writes.js +48 -0
  57. package/dist/src/ui/tui/text.js +158 -4
  58. package/dist/src/ui/tui/user-input.js +568 -0
  59. package/dist/src/utils.js +9 -0
  60. package/package.json +20 -8
  61. package/dist/src/markdown-renderer.js +0 -112
  62. package/dist/src/project-index.js +0 -221
  63. package/dist/src/tools/code-intel.js +0 -472
  64. package/dist/src/tools/find-symbol.js +0 -70
  65. package/dist/src/tools/hover-symbol.js +0 -95
  66. package/dist/src/tools/list-symbols.js +0 -55
  67. package/dist/src/tools/search-code.js +0 -37
  68. package/dist/src/tools/signature-help.js +0 -118
@@ -1,12 +1,16 @@
1
1
  import { drainBackgroundJobNotifications } from '../background-jobs.js';
2
2
  import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
3
- import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js';
3
+ import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '../session-store.js';
4
4
  import { executeLocalToolCall } from '../tool-executor.js';
5
- import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
5
+ import { saveGeneratedImage } from '../tools/save-generated-image.js';
6
+ import { isUserInputQuestionArray } from './contracts.js';
7
+ import { createTraceContext, gatewayFailureCategory, transportFailureDetail, normalizeServerUrl, readErrorResponse, } from './http.js';
6
8
  import { collectClientEnvironment } from '../client-environment.js';
7
9
  import { collectProjectOrientation } from '../project-orientation.js';
10
+ import { describeSessionImageStore } from '../core/session-image-store.js';
8
11
  import { autoAttachImages } from '../core/image-path-extractor.js';
9
12
  import { formatTurnFailureMarker } from '../turn-failure-marker.js';
13
+ import { browserExtensionSeen, releaseBrowserAfterTurn } from '../browser/session-bridge.js';
10
14
  export class TurnCancelledError extends Error {
11
15
  name = 'TurnCancelledError';
12
16
  constructor(message = 'Turn cancelled.') {
@@ -18,11 +22,13 @@ export class ChatTurnFailedError extends Error {
18
22
  category;
19
23
  retryable;
20
24
  traceId;
21
- constructor(message, category = 'unknown_error', retryable = false, traceId = '') {
25
+ partialSnapshot;
26
+ constructor(message, category = 'unknown_error', retryable = false, traceId = '', partialSnapshot) {
22
27
  super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
23
28
  this.category = category;
24
29
  this.retryable = retryable;
25
30
  this.traceId = traceId;
31
+ this.partialSnapshot = partialSnapshot;
26
32
  }
27
33
  }
28
34
  export function isTurnCancelledError(error) {
@@ -52,6 +58,7 @@ function parseSseBlock(block) {
52
58
  return { event, data: text };
53
59
  }
54
60
  }
61
+ let toolStateSeqCounter = 0;
55
62
  function toolStateFromSession(session) {
56
63
  return {
57
64
  autoYes: session.autoYes,
@@ -94,18 +101,43 @@ export function preserveCancelledTurnInput(session, input) {
94
101
  return;
95
102
  break;
96
103
  }
97
- session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
104
+ session.history.push({
105
+ role: 'user',
106
+ parts: [{ text }],
107
+ kind: 'turnStart',
108
+ userInput: text,
109
+ });
110
+ }
111
+ function appendTurnFailureMarker(session, category) {
112
+ session.history.push({
113
+ role: 'model',
114
+ parts: [{ text: formatTurnFailureMarker(category) }],
115
+ });
98
116
  }
99
117
  function preserveFailedTurnInput(session, input, category) {
100
118
  const text = input.trim();
101
119
  if (!text)
102
120
  return;
103
- session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
104
121
  session.history.push({
105
- role: 'model',
106
- parts: [{ text: formatTurnFailureMarker(category) }],
122
+ role: 'user',
123
+ parts: [{ text }],
124
+ kind: 'turnStart',
125
+ userInput: text,
126
+ });
127
+ appendTurnFailureMarker(session, category);
128
+ }
129
+ function tagTurnPhase(phase, run) {
130
+ return run().catch((error) => {
131
+ if (error && typeof error === 'object' && !error.thegitaiPhase) {
132
+ error.thegitaiPhase = phase;
133
+ }
134
+ throw error;
107
135
  });
108
136
  }
137
+ function turnPhaseOf(error) {
138
+ const phase = error?.thegitaiPhase;
139
+ return typeof phase === 'string' ? phase : null;
140
+ }
109
141
  function historyHasToolCall(session, callId) {
110
142
  return session.history.some((entry) => (entry.parts ?? []).some((part) => String(part?.functionCall?.id ?? '') === callId));
111
143
  }
@@ -144,11 +176,19 @@ function publicStatusMessage(data) {
144
176
  ? event.toolName
145
177
  : 'tool';
146
178
  if (event.phase === 'thinking')
147
- return 'Thinking...';
179
+ return 'Exploring options...';
180
+ if (event.phase === 'analyzing_image') {
181
+ return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
182
+ }
183
+ if (event.phase === 'generating_image') {
184
+ return 'Generating image...';
185
+ }
148
186
  if (event.phase === 'running_tool')
149
187
  return `Running ${toolName}...`;
150
188
  if (event.phase === 'waiting_for_tool')
151
189
  return `Running ${toolName} locally...`;
190
+ if (event.phase === 'waiting_for_user_input')
191
+ return 'Waiting for your input...';
152
192
  return null;
153
193
  }
154
194
  function normalizeShellJobToolCall(call) {
@@ -185,9 +225,32 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
185
225
  toolCallId: event.call.id,
186
226
  result,
187
227
  toolState: toolStateFromSession(session),
228
+ toolStateSeq: ++toolStateSeqCounter,
229
+ };
230
+ const trace = createTraceContext(traceId);
231
+ const response = await tagTurnPhase('posting a tool result', () => fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
232
+ method: 'POST',
233
+ headers: {
234
+ authorization: `Bearer ${config.token}`,
235
+ 'content-type': 'application/json',
236
+ ...trace.headers,
237
+ },
238
+ body: JSON.stringify(payload),
239
+ }));
240
+ if (response.status === 410) {
241
+ return;
242
+ }
243
+ if (!response.ok) {
244
+ throw await readErrorResponse(response, trace.traceId);
245
+ }
246
+ }
247
+ async function postUserInputResult({ config, turnId, requestId, result, fetchImpl, traceId, }) {
248
+ const payload = {
249
+ requestId,
250
+ result,
188
251
  };
189
252
  const trace = createTraceContext(traceId);
190
- const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
253
+ const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/user-input-result`, {
191
254
  method: 'POST',
192
255
  headers: {
193
256
  authorization: `Bearer ${config.token}`,
@@ -203,7 +266,54 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
203
266
  throw await readErrorResponse(response, trace.traceId);
204
267
  }
205
268
  }
206
- async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
269
+ export async function postInterjection({ config, turnId, text, messageId, imageAttachments = [], fetchImpl = globalThis.fetch, traceId, }) {
270
+ const payload = {
271
+ text,
272
+ messageId,
273
+ ...(imageAttachments.length > 0 ? { imageAttachments } : {}),
274
+ };
275
+ const trace = createTraceContext(traceId);
276
+ const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/interject`, {
277
+ method: 'POST',
278
+ headers: {
279
+ authorization: `Bearer ${config.token}`,
280
+ 'content-type': 'application/json',
281
+ ...trace.headers,
282
+ },
283
+ body: JSON.stringify(payload),
284
+ });
285
+ if (response.status === 410) {
286
+ return 'stale';
287
+ }
288
+ if (!response.ok) {
289
+ throw await readErrorResponse(response, trace.traceId);
290
+ }
291
+ return 'delivered';
292
+ }
293
+ const turnIdOverrides = new WeakMap();
294
+ function enterServerTurnId(session, serverSessionTurnId) {
295
+ const active = turnIdOverrides.get(session);
296
+ if (active) {
297
+ active.depth += 1;
298
+ return;
299
+ }
300
+ turnIdOverrides.set(session, {
301
+ previousTurnId: session.turnState.id,
302
+ depth: 1,
303
+ });
304
+ session.turnState.id = serverSessionTurnId;
305
+ }
306
+ function exitServerTurnId(session) {
307
+ const active = turnIdOverrides.get(session);
308
+ if (!active)
309
+ return;
310
+ active.depth -= 1;
311
+ if (active.depth === 0) {
312
+ session.turnState.id = active.previousTurnId;
313
+ turnIdOverrides.delete(session);
314
+ }
315
+ }
316
+ async function executeAndPostToolResult({ config, session, event, input, fetchImpl, signal, traceId, }) {
207
317
  const turnId = String(event?.turnId ?? '').trim();
208
318
  if (!turnId || !event?.call?.id || !event.call.name) {
209
319
  throw new Error('Server emitted an invalid tool-call event.');
@@ -211,17 +321,23 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
211
321
  if (signal?.aborted) {
212
322
  throw new TurnCancelledError();
213
323
  }
214
- const previousTurnId = session.turnState.id;
215
324
  const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
216
325
  if (serverSessionTurnId) {
217
- session.turnState.id = serverSessionTurnId;
326
+ enterServerTurnId(session, serverSessionTurnId);
218
327
  if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
219
328
  createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
220
329
  }
221
330
  }
222
331
  try {
223
332
  const call = normalizeShellJobToolCall(event.call);
224
- const rawResult = await executeLocalToolCall({ projectIndex }, session, call);
333
+ const rawResult = event.generatedImage
334
+ ? saveGeneratedImage({
335
+ base64Data: event.generatedImage.base64Data,
336
+ mimeType: event.generatedImage.mimeType,
337
+ suggestedFilename: event.generatedImage.suggestedFilename ||
338
+ String(call.args?.filename ?? call.args?.file_name ?? ''),
339
+ })
340
+ : await executeLocalToolCall(session, call);
225
341
  preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
226
342
  if (signal?.aborted) {
227
343
  throw new TurnCancelledError();
@@ -237,10 +353,12 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
237
353
  });
238
354
  }
239
355
  finally {
240
- session.turnState.id = previousTurnId;
356
+ if (serverSessionTurnId) {
357
+ exitServerTurnId(session);
358
+ }
241
359
  }
242
360
  }
243
- async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
361
+ async function consumeTurnStream({ response, config, session, input, fetchImpl, signal, traceId, onTurnStart, onInterjectionDelivered, }) {
244
362
  if (!response.body) {
245
363
  throw new Error('Server returned an empty chat stream.');
246
364
  }
@@ -250,8 +368,56 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
250
368
  const finalResult = {
251
369
  current: null,
252
370
  };
371
+ const pendingParallelTools = [];
372
+ let firstParallelFailure = null;
373
+ let rejectOnParallelFailure = null;
374
+ const parallelToolFailure = new Promise((_, reject) => {
375
+ rejectOnParallelFailure = reject;
376
+ });
377
+ parallelToolFailure.catch(() => { });
378
+ function recordParallelFailure(error) {
379
+ const failure = error ?? new Error('Local tool execution failed.');
380
+ if (firstParallelFailure == null) {
381
+ firstParallelFailure = failure;
382
+ rejectOnParallelFailure?.(failure);
383
+ }
384
+ return failure;
385
+ }
386
+ async function drainParallelTools() {
387
+ if (!pendingParallelTools.length)
388
+ return;
389
+ const pending = pendingParallelTools.splice(0);
390
+ const outcomes = await Promise.all(pending);
391
+ for (const outcome of outcomes) {
392
+ if (outcome != null)
393
+ throw outcome;
394
+ }
395
+ }
253
396
  async function handleEvent(event) {
397
+ if (event.event === 'turn-start') {
398
+ const turnId = String(event.data?.turnId ?? '').trim();
399
+ if (turnId)
400
+ onTurnStart?.(turnId);
401
+ return;
402
+ }
403
+ if (event.event === 'interjection-delivered') {
404
+ onInterjectionDelivered?.(event.data);
405
+ return;
406
+ }
254
407
  if (event.event === 'status') {
408
+ const data = event.data;
409
+ if (data?.phase === 'analyzing_image') {
410
+ session.onImageAnalysis?.(Math.max(1, Number(data.imageCount ?? 1) || 1));
411
+ session.onImageGeneration?.(false);
412
+ }
413
+ else if (data?.phase === 'generating_image') {
414
+ session.onImageGeneration?.(true);
415
+ session.onImageAnalysis?.(0);
416
+ }
417
+ else if (data?.phase) {
418
+ session.onImageAnalysis?.(0);
419
+ session.onImageGeneration?.(false);
420
+ }
255
421
  const message = publicStatusMessage(event.data);
256
422
  if (message)
257
423
  session.onStatus(message);
@@ -261,11 +427,24 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
261
427
  return;
262
428
  }
263
429
  if (event.event === 'tool-call') {
430
+ const data = event.data;
431
+ if (data?.parallelSafe === true) {
432
+ pendingParallelTools.push(executeAndPostToolResult({
433
+ config,
434
+ session,
435
+ event: data,
436
+ input,
437
+ fetchImpl,
438
+ signal,
439
+ traceId,
440
+ }).then(() => null, (error) => recordParallelFailure(error)));
441
+ return;
442
+ }
443
+ await drainParallelTools();
264
444
  await executeAndPostToolResult({
265
445
  config,
266
- projectIndex,
267
446
  session,
268
- event: event.data,
447
+ event: data,
269
448
  input,
270
449
  fetchImpl,
271
450
  signal,
@@ -280,36 +459,75 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
280
459
  }
281
460
  return;
282
461
  }
462
+ if (event.event === 'user-input-request') {
463
+ await drainParallelTools();
464
+ const data = event.data;
465
+ const turnId = String(data?.turnId ?? '').trim();
466
+ const requestId = String(data?.requestId ?? '').trim();
467
+ if (!turnId ||
468
+ !requestId ||
469
+ !isUserInputQuestionArray(data?.questions)) {
470
+ throw new Error('Server emitted an invalid user-input request.');
471
+ }
472
+ if (!session.requestUserInput) {
473
+ throw new Error('Interactive user input is unavailable in this client.');
474
+ }
475
+ const result = await session.requestUserInput({ questions: data.questions }, signal);
476
+ if (signal?.aborted) {
477
+ throw new TurnCancelledError();
478
+ }
479
+ await postUserInputResult({
480
+ config,
481
+ turnId,
482
+ requestId,
483
+ result,
484
+ fetchImpl,
485
+ traceId,
486
+ });
487
+ return;
488
+ }
283
489
  if (event.event === 'result') {
490
+ await drainParallelTools();
284
491
  finalResult.current = event.data;
285
492
  return;
286
493
  }
287
494
  if (event.event === 'cancelled' || event.event === 'error') {
495
+ await drainParallelTools().catch(() => { });
288
496
  const message = String(event.data?.message ?? 'Server chat failed.');
289
497
  if (event.event === 'cancelled') {
290
498
  throw new TurnCancelledError(message);
291
499
  }
292
- throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId);
500
+ throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId, event.data?.snapshot);
293
501
  }
294
502
  }
295
- while (true) {
296
- if (signal?.aborted) {
297
- await reader.cancel().catch(() => { });
298
- throw new TurnCancelledError();
299
- }
300
- const read = await reader.read();
301
- if (read.done)
302
- break;
303
- buffer += decoder.decode(read.value, { stream: true });
304
- let separatorIndex = buffer.indexOf('\n\n');
305
- while (separatorIndex !== -1) {
306
- const block = buffer.slice(0, separatorIndex);
307
- buffer = buffer.slice(separatorIndex + 2);
308
- const event = parseSseBlock(block);
309
- if (event)
310
- await handleEvent(event);
311
- separatorIndex = buffer.indexOf('\n\n');
503
+ try {
504
+ while (true) {
505
+ if (signal?.aborted) {
506
+ await reader.cancel().catch(() => { });
507
+ throw new TurnCancelledError();
508
+ }
509
+ const read = await Promise.race([reader.read(), parallelToolFailure]);
510
+ if (read.done)
511
+ break;
512
+ buffer += decoder.decode(read.value, { stream: true });
513
+ let separatorIndex = buffer.indexOf('\n\n');
514
+ while (separatorIndex !== -1) {
515
+ const block = buffer.slice(0, separatorIndex);
516
+ buffer = buffer.slice(separatorIndex + 2);
517
+ const event = parseSseBlock(block);
518
+ if (event)
519
+ await handleEvent(event);
520
+ separatorIndex = buffer.indexOf('\n\n');
521
+ }
312
522
  }
523
+ await drainParallelTools();
524
+ }
525
+ catch (error) {
526
+ await reader.cancel().catch(() => { });
527
+ throw error;
528
+ }
529
+ finally {
530
+ await drainParallelTools().catch(() => { });
313
531
  }
314
532
  buffer += decoder.decode();
315
533
  const tail = buffer.trim();
@@ -323,7 +541,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
323
541
  }
324
542
  return finalResult.current;
325
543
  }
326
- export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, }) {
544
+ export async function sendServerUserMessage({ config, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, }) {
327
545
  const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
328
546
  const requestImageAttachments = autoAttach.attachments.length > 0
329
547
  ? [...imageAttachments, ...autoAttach.attachments]
@@ -342,8 +560,11 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
342
560
  backgroundJobUpdate: backgroundJobUpdate || undefined,
343
561
  clientEnvironment: collectClientEnvironment({ env: session.env }),
344
562
  projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
563
+ sessionImageStore: describeSessionImageStore(session.env) ?? undefined,
345
564
  imageAttachments: imageAttachmentsForServer(requestImageAttachments),
346
- maxToolSteps: session.maxToolSteps,
565
+ ...(session.maxToolStepsWasExplicit || !browserExtensionSeen(session.env)
566
+ ? { maxToolSteps: session.maxToolSteps }
567
+ : {}),
347
568
  autoYes: session.autoYes,
348
569
  agentMode: session.agentMode,
349
570
  };
@@ -357,7 +578,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
357
578
  signal?.addEventListener('abort', preserveOnAbort, { once: true });
358
579
  }
359
580
  try {
360
- const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn`, {
581
+ const response = await tagTurnPhase('opening the turn', () => fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn`, {
361
582
  method: 'POST',
362
583
  headers: {
363
584
  accept: 'text/event-stream',
@@ -367,20 +588,21 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
367
588
  },
368
589
  body: JSON.stringify(request),
369
590
  signal,
370
- });
591
+ }));
371
592
  if (!response.ok) {
372
593
  throw await readErrorResponse(response, trace.traceId);
373
594
  }
374
- const result = await consumeTurnStream({
595
+ const result = await tagTurnPhase('reading the turn stream', () => consumeTurnStream({
375
596
  response,
376
597
  config,
377
- projectIndex,
378
598
  session,
379
599
  input: requestInputBase,
380
600
  fetchImpl,
381
601
  signal,
382
602
  traceId: trace.traceId,
383
- });
603
+ onTurnStart,
604
+ onInterjectionDelivered,
605
+ }));
384
606
  applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
385
607
  return {
386
608
  text: result.text,
@@ -396,11 +618,31 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
396
618
  ? error
397
619
  : new TurnCancelledError();
398
620
  }
399
- session.history.length = preTurnHistoryLength;
400
- preserveFailedTurnInput(session, requestInputBase, error instanceof ChatTurnFailedError ? error.category : 'unknown_error');
621
+ const transportDetail = error instanceof ChatTurnFailedError ? null : transportFailureDetail(error);
622
+ if (transportDetail && error instanceof Error) {
623
+ const phase = turnPhaseOf(error) ?? 'reaching the server';
624
+ error.message = `connection lost while ${phase}: ${transportDetail} [trace ${trace.traceId}]`;
625
+ }
626
+ const category = error instanceof ChatTurnFailedError
627
+ ? error.category
628
+ :
629
+ gatewayFailureCategory(error) ?? 'unknown_error';
630
+ const partial = error instanceof ChatTurnFailedError ? error.partialSnapshot : undefined;
631
+ if (partial) {
632
+ applySessionSnapshot(session, partial, { preserveAgentMode: true });
633
+ }
634
+ else {
635
+ session.history.length = preTurnHistoryLength;
636
+ preserveFailedTurnInput(session, requestInputBase, category);
637
+ }
638
+ try {
639
+ saveSessionState(session, session.env);
640
+ }
641
+ catch { }
401
642
  throw error;
402
643
  }
403
644
  finally {
404
645
  signal?.removeEventListener('abort', preserveOnAbort);
646
+ releaseBrowserAfterTurn();
405
647
  }
406
648
  }
@@ -1 +1,55 @@
1
- export {};
1
+ function nonEmptyString(value) {
2
+ return typeof value === 'string' && value.trim().length > 0;
3
+ }
4
+ const USER_INPUT_ID_PATTERN = /^[a-z][a-z0-9_]*$/;
5
+ export function isUserInputQuestionArray(value) {
6
+ if (!Array.isArray(value) || value.length < 1 || value.length > 4) {
7
+ return false;
8
+ }
9
+ const questionIds = new Set();
10
+ return value.every((question) => {
11
+ if (!question || typeof question !== 'object' || Array.isArray(question)) {
12
+ return false;
13
+ }
14
+ const candidate = question;
15
+ if (!nonEmptyString(candidate.id) ||
16
+ !USER_INPUT_ID_PATTERN.test(candidate.id) ||
17
+ questionIds.has(candidate.id) ||
18
+ !nonEmptyString(candidate.header) ||
19
+ Array.from(candidate.header).length > 12 ||
20
+ !nonEmptyString(candidate.question) ||
21
+ typeof candidate.multiSelect !== 'boolean' ||
22
+ !Array.isArray(candidate.options) ||
23
+ candidate.options.length < 2 ||
24
+ candidate.options.length > 4) {
25
+ return false;
26
+ }
27
+ questionIds.add(candidate.id);
28
+ const optionIds = new Set();
29
+ const optionsValid = candidate.options.every((option) => {
30
+ if (!option || typeof option !== 'object' || Array.isArray(option)) {
31
+ return false;
32
+ }
33
+ const item = option;
34
+ if (!nonEmptyString(item.id) ||
35
+ !USER_INPUT_ID_PATTERN.test(item.id) ||
36
+ optionIds.has(item.id) ||
37
+ !nonEmptyString(item.label) ||
38
+ !nonEmptyString(item.description)) {
39
+ return false;
40
+ }
41
+ optionIds.add(item.id);
42
+ return true;
43
+ });
44
+ if (!optionsValid) {
45
+ return false;
46
+ }
47
+ if (candidate.recommendedOptionId !== undefined &&
48
+ (!nonEmptyString(candidate.recommendedOptionId) ||
49
+ candidate.recommendedOptionId !==
50
+ candidate.options[0].id)) {
51
+ return false;
52
+ }
53
+ return true;
54
+ });
55
+ }
@@ -0,0 +1 @@
1
+ export const DEFAULT_THEGITAI_HOST = 'https://thegit.ai';