remoat 0.2.9 → 0.2.10

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.
package/dist/bot/index.js CHANGED
@@ -27,9 +27,9 @@ const antigravityLauncher_1 = require("../services/antigravityLauncher");
27
27
  const pathUtils_1 = require("../utils/pathUtils");
28
28
  const promptDispatcher_1 = require("../services/promptDispatcher");
29
29
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
30
- const streamMessageFormatter_1 = require("../utils/streamMessageFormatter");
30
+ const assistantDomExtractor_1 = require("../services/assistantDomExtractor");
31
31
  const telegramFormatter_1 = require("../utils/telegramFormatter");
32
- const processLogBuffer_1 = require("../utils/processLogBuffer");
32
+ // ProcessLogBuffer no longer used — progress display uses ordered event stream
33
33
  const imageHandler_1 = require("../utils/imageHandler");
34
34
  const voiceHandler_1 = require("../utils/voiceHandler");
35
35
  const modeUi_1 = require("../ui/modeUi");
@@ -69,7 +69,7 @@ function stripHtmlForFile(html) {
69
69
  // Links
70
70
  text = text.replace(/<a\s+href="([^"]*)">([\s\S]*?)<\/a>/gi, '[$2]($1)');
71
71
  // Blockquotes
72
- text = text.replace(/<blockquote>([\s\S]*?)<\/blockquote>/gi, (_m, content) => content.split('\n').map((l) => `> ${l}`).join('\n'));
72
+ text = text.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_m, content) => content.split('\n').map((l) => `> ${l}`).join('\n'));
73
73
  // Strip remaining tags
74
74
  text = text.replace(/<[^>]+>/g, '');
75
75
  // Decode entities
@@ -142,7 +142,9 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
142
142
  /** Send a potentially long response, splitting into chunks and attaching a .md file if needed. */
143
143
  const sendChunkedResponse = async (title, footer, rawBody, isAlreadyHtml) => {
144
144
  const formattedBody = isAlreadyHtml ? rawBody : (0, telegramFormatter_1.formatForTelegram)(rawBody);
145
- const fullMsg = `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n${formattedBody}\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>`;
145
+ const titleLine = title ? `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n` : '';
146
+ const footerLine = footer ? `\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>` : '';
147
+ const fullMsg = `${titleLine}${formattedBody}${footerLine}`;
146
148
  if (fullMsg.length <= TELEGRAM_MSG_LIMIT) {
147
149
  await upsertLiveResponse(title, rawBody, footer, { expectedVersion: liveResponseUpdateVersion, isAlreadyHtml, skipTruncation: true });
148
150
  return;
@@ -154,10 +156,12 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
154
156
  for (let pi = 0; pi < inlineCount; pi++) {
155
157
  const partLabel = hasFile ? `(${pi + 1}/${inlineCount}+file)` : `(${pi + 1}/${total})`;
156
158
  if (pi === 0) {
157
- await upsertLiveResponse(`${title} ${partLabel}`, bodyChunks[pi], footer, { expectedVersion: liveResponseUpdateVersion, isAlreadyHtml: true, skipTruncation: true });
159
+ const firstTitle = title ? `${title} ${partLabel}` : partLabel;
160
+ await upsertLiveResponse(firstTitle, bodyChunks[pi], footer, { expectedVersion: liveResponseUpdateVersion, isAlreadyHtml: true, skipTruncation: true });
158
161
  }
159
162
  else {
160
- await sendMsg(`${bodyChunks[pi]}\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)} ${partLabel}</i>`);
163
+ const partFooter = footer ? `${(0, telegramFormatter_1.escapeHtml)(footer)} ${partLabel}` : partLabel;
164
+ await sendMsg(`${bodyChunks[pi]}\n\n<i>${partFooter}</i>`);
161
165
  }
162
166
  }
163
167
  if (hasFile) {
@@ -182,60 +186,102 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
182
186
  const localMode = modeService.getCurrentMode();
183
187
  const modeName = modeService_1.MODE_UI_NAMES[localMode] || localMode;
184
188
  const currentModel = (await cdp.getCurrentModel()) || modelService.getCurrentModel();
185
- await sendEmbed(`${PHASE_ICONS.sending} [${modeName} - ${currentModel}] Sending...`, (0, streamMessageFormatter_1.buildModeModelLines)(modeName, currentModel, currentModel).join('\n'));
189
+ const modelLabel = `${currentModel}`;
190
+ // Initialize live progress message (replaces separate "Sending" embed)
191
+ let liveActivityMsgId = null;
192
+ try {
193
+ const sendingText = `<b>${PHASE_ICONS.sending} ${(0, telegramFormatter_1.escapeHtml)(modeName)} · ${(0, telegramFormatter_1.escapeHtml)(modelLabel)}</b>\n\n<i>Sending...</i>`;
194
+ const sendingMsg = await api.sendMessage(channel.chatId, sendingText, { parse_mode: 'HTML', message_thread_id: channel.threadId });
195
+ liveActivityMsgId = sendingMsg.message_id;
196
+ }
197
+ catch (e) {
198
+ logger_1.logger.error('[sendPrompt] Failed to send initial status:', e);
199
+ }
186
200
  let isFinalized = false;
187
201
  let elapsedTimer = null;
188
202
  let lastProgressText = '';
189
- let lastActivityLogText = '';
190
- let lastThinkingLogText = '';
191
203
  const LIVE_RESPONSE_MAX_LEN = 3800;
192
- const LIVE_ACTIVITY_MAX_LEN = 3800;
193
- const THINKING_BUDGET = 1500;
194
- const ACTIVITY_BUDGET = 2300;
195
- const processLogBuffer = new processLogBuffer_1.ProcessLogBuffer({ maxChars: LIVE_ACTIVITY_MAX_LEN, maxEntries: 120, maxEntryLength: 220 });
204
+ const MAX_PROGRESS_BODY = 3500;
205
+ const MAX_PROGRESS_ENTRIES = 60;
196
206
  let liveResponseMsgId = null;
197
- let liveActivityMsgId = null;
198
207
  let lastLiveResponseKey = '';
199
208
  let lastLiveActivityKey = '';
200
209
  let liveResponseUpdateVersion = 0;
201
210
  let liveActivityUpdateVersion = 0;
202
- const ACTIVITY_PLACEHOLDER = (0, i18n_1.t)('Collecting process logs...');
211
+ const progressLog = [];
212
+ let thinkingActive = false;
213
+ const thinkingContentParts = [];
214
+ let lastThoughtLabel = '';
215
+ /** Check if text is junk (numbers, very short, not meaningful) */
216
+ const isJunkEntry = (text) => {
217
+ const t = text.trim();
218
+ if (t.length < 5)
219
+ return true;
220
+ if (/^\d+$/.test(t))
221
+ return true;
222
+ // Single word under 8 chars without context (e.g. "Analyzed" alone)
223
+ if (!/\s/.test(t) && t.length < 8)
224
+ return true;
225
+ return false;
226
+ };
227
+ /** Format a single activity line — collapse multi-line text into one line */
228
+ const formatActivityLine = (raw) => {
229
+ // Collapse newlines into spaces so file references after verbs aren't lost
230
+ // e.g. "Analyzed\npackage.json#L1-75" → "Analyzed package.json#L1-75"
231
+ const collapsed = (raw || '').replace(/\n+/g, ' ').replace(/\s{2,}/g, ' ').trim();
232
+ if (!collapsed || isJunkEntry(collapsed))
233
+ return '';
234
+ return (0, telegramFormatter_1.escapeHtml)(collapsed.slice(0, 120));
235
+ };
236
+ /** Trim progress log to stay within size limits */
237
+ const trimProgressLog = () => {
238
+ while (progressLog.length > MAX_PROGRESS_ENTRIES)
239
+ progressLog.shift();
240
+ };
241
+ /** Build the progress message body from the ordered event stream */
242
+ const buildProgressBody = () => {
243
+ const lines = [];
244
+ for (const e of progressLog) {
245
+ switch (e.kind) {
246
+ case 'thought':
247
+ lines.push(`💭 <i>${(0, telegramFormatter_1.escapeHtml)(e.text)}</i>`);
248
+ break;
249
+ case 'thought-content':
250
+ lines.push(`<i>${(0, telegramFormatter_1.escapeHtml)(e.text)}</i>`);
251
+ break;
252
+ case 'activity':
253
+ lines.push(e.text); // already HTML-escaped
254
+ break;
255
+ }
256
+ }
257
+ if (thinkingActive) {
258
+ lines.push('💭 <i>Thinking...</i>');
259
+ }
260
+ // Use \n\n for spacing between entries (like Antigravity's line gap)
261
+ let body = lines.join('\n\n');
262
+ // Trim from beginning if too long, keeping most recent events
263
+ if (body.length > MAX_PROGRESS_BODY) {
264
+ body = '...\n\n' + body.slice(-MAX_PROGRESS_BODY + 5);
265
+ }
266
+ return body || '<i>Generating...</i>';
267
+ };
268
+ /** Build full progress message with title + body + footer */
269
+ const buildProgressMessage = (title, footer) => {
270
+ const body = buildProgressBody();
271
+ const footerLine = footer ? `\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>` : '';
272
+ return `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n${body}${footerLine}`;
273
+ };
203
274
  const buildLiveResponseText = (title, rawText, footer, isAlreadyHtml = false, skipTruncation = false) => {
204
275
  const normalized = (rawText || '').trim();
205
276
  const body = normalized
206
277
  ? (isAlreadyHtml ? normalized : (0, telegramFormatter_1.formatForTelegram)(normalized))
207
- : (0, i18n_1.t)('Waiting for output...');
278
+ : (0, i18n_1.t)('Generating...');
208
279
  const truncated = (!skipTruncation && body.length > LIVE_RESPONSE_MAX_LEN)
209
280
  ? '...(beginning truncated)\n' + body.slice(-LIVE_RESPONSE_MAX_LEN + 30)
210
281
  : body;
211
- return `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n${truncated}\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>`;
212
- };
213
- const buildLiveActivityText = (title, rawText, footer) => {
214
- const normalized = (rawText || '').trim();
215
- const thinkingNormalized = (lastThinkingLogText || '').trim();
216
- const hasThinking = thinkingNormalized.length > 10;
217
- let body;
218
- if (hasThinking) {
219
- const thinkingTruncated = thinkingNormalized.length > THINKING_BUDGET
220
- ? '...' + thinkingNormalized.slice(-THINKING_BUDGET + 3)
221
- : thinkingNormalized;
222
- const activityBody = normalized
223
- ? (0, streamMessageFormatter_1.fitForSingleEmbedDescription)((0, telegramFormatter_1.formatForTelegram)(normalized), ACTIVITY_BUDGET)
224
- : ACTIVITY_PLACEHOLDER;
225
- body = `\u{1F9E0} <b>AI Thinking</b>\n<blockquote>${(0, telegramFormatter_1.escapeHtml)(thinkingTruncated)}</blockquote>\n\n\u{1F4CB} <b>Activity</b>\n${activityBody}`;
226
- }
227
- else {
228
- body = normalized
229
- ? (0, streamMessageFormatter_1.fitForSingleEmbedDescription)((0, telegramFormatter_1.formatForTelegram)(normalized), LIVE_ACTIVITY_MAX_LEN)
230
- : ACTIVITY_PLACEHOLDER;
231
- }
232
- return `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n${body}\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>`;
233
- };
234
- const appendProcessLogs = (text) => {
235
- const normalized = (text || '').trim();
236
- if (!normalized)
237
- return processLogBuffer.snapshot();
238
- return processLogBuffer.append(normalized);
282
+ const titleLine = title ? `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n` : '';
283
+ const footerLine = footer ? `\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>` : '';
284
+ return `${titleLine}${truncated}${footerLine}`;
239
285
  };
240
286
  const upsertLiveResponse = (title, rawText, footer, opts) => enqueueResponse(async () => {
241
287
  if (opts?.skipWhenFinalized && isFinalized)
@@ -254,16 +300,18 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
254
300
  liveResponseMsgId = await sendMsg(text);
255
301
  }
256
302
  }, 'upsert-response');
257
- const upsertLiveActivity = (title, rawText, footer, opts) => enqueueActivity(async () => {
303
+ /** Refresh progress message using the ordered event stream */
304
+ const refreshProgress = (title, footer, opts) => enqueueActivity(async () => {
258
305
  if (opts?.skipWhenFinalized && isFinalized)
259
306
  return;
260
307
  if (opts?.expectedVersion !== undefined && opts.expectedVersion !== liveActivityUpdateVersion)
261
308
  return;
262
- const text = buildLiveActivityText(title, rawText, footer);
263
- const renderKey = `${title}|${rawText.slice(0, 200)}|${footer}`;
264
- if (renderKey === lastLiveActivityKey && liveActivityMsgId)
309
+ const text = buildProgressMessage(title, footer);
310
+ // Use progress body hash for dedup
311
+ const bodySnap = progressLog.length + '|' + thinkingActive + '|' + title + '|' + footer;
312
+ if (bodySnap === lastLiveActivityKey && liveActivityMsgId)
265
313
  return;
266
- lastLiveActivityKey = renderKey;
314
+ lastLiveActivityKey = bodySnap;
267
315
  if (liveActivityMsgId) {
268
316
  await editMsg(liveActivityMsgId, text);
269
317
  }
@@ -271,6 +319,18 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
271
319
  liveActivityMsgId = await sendMsg(text);
272
320
  }
273
321
  }, 'upsert-activity');
322
+ /** Direct message update for special cases (completion, quota, timeout) */
323
+ const setProgressMessage = (htmlContent, opts) => enqueueActivity(async () => {
324
+ if (opts?.expectedVersion !== undefined && opts.expectedVersion !== liveActivityUpdateVersion)
325
+ return;
326
+ lastLiveActivityKey = htmlContent.slice(0, 200);
327
+ if (liveActivityMsgId) {
328
+ await editMsg(liveActivityMsgId, htmlContent);
329
+ }
330
+ else {
331
+ liveActivityMsgId = await sendMsg(htmlContent);
332
+ }
333
+ }, 'upsert-activity');
274
334
  const sendGeneratedImages = async (responseText) => {
275
335
  const imageIntentPattern = /(image|images|png|jpg|jpeg|gif|webp|illustration|diagram|render)/i;
276
336
  const imageUrlPattern = /https?:\/\/\S+\.(png|jpg|jpeg|gif|webp)/i;
@@ -338,7 +398,15 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
338
398
  return;
339
399
  }
340
400
  const startTime = Date.now();
341
- await upsertLiveActivity(`${PHASE_ICONS.thinking} Process Log`, '', (0, i18n_1.t)('⏱️ Elapsed: 0s | Process log'));
401
+ const progressTitle = () => `${PHASE_ICONS.thinking} ${modelLabel}`;
402
+ const progressFooter = () => `⏱️ ${Math.round((Date.now() - startTime) / 1000)}s`;
403
+ /** Trigger a progress message refresh */
404
+ const triggerProgressRefresh = () => {
405
+ liveActivityUpdateVersion += 1;
406
+ const v = liveActivityUpdateVersion;
407
+ refreshProgress(progressTitle(), progressFooter(), { expectedVersion: v, skipWhenFinalized: true }).catch(() => { });
408
+ };
409
+ await refreshProgress(progressTitle(), progressFooter());
342
410
  monitor = new responseMonitor_1.ResponseMonitor({
343
411
  cdpService: cdp,
344
412
  pollIntervalMs: 2000,
@@ -348,27 +416,59 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
348
416
  onProcessLog: (logText) => {
349
417
  if (isFinalized)
350
418
  return;
351
- if (logText && logText.trim().length > 0)
352
- lastActivityLogText = appendProcessLogs(logText);
353
- const elapsed = Math.round((Date.now() - startTime) / 1000);
354
- liveActivityUpdateVersion += 1;
355
- const v = liveActivityUpdateVersion;
356
- upsertLiveActivity(`${PHASE_ICONS.thinking} Process Log`, lastActivityLogText || ACTIVITY_PLACEHOLDER, (0, i18n_1.t)(`⏱️ Elapsed: ${elapsed}s | Process log`), { expectedVersion: v, skipWhenFinalized: true }).catch(() => { });
419
+ const trimmed = (logText || '').trim();
420
+ if (!trimmed || isJunkEntry(trimmed))
421
+ return;
422
+ const formatted = formatActivityLine(trimmed);
423
+ if (formatted) {
424
+ progressLog.push({ kind: 'activity', text: formatted });
425
+ trimProgressLog();
426
+ triggerProgressRefresh();
427
+ }
357
428
  },
358
429
  onThinkingLog: (thinkingText) => {
359
430
  if (isFinalized)
360
431
  return;
361
- logger_1.logger.debug('[Bot] onThinkingLog received:', (thinkingText || '').slice(0, 100));
362
- if (thinkingText && thinkingText.trim().length > 0) {
363
- lastThinkingLogText = lastThinkingLogText
364
- ? lastThinkingLogText + '\n' + thinkingText.trim()
365
- : thinkingText.trim();
366
- logger_1.logger.debug('[Bot] lastThinkingLogText now:', lastThinkingLogText.length, 'chars');
432
+ const trimmed = (thinkingText || '').trim();
433
+ if (!trimmed)
434
+ return;
435
+ logger_1.logger.debug('[Bot] onThinkingLog received:', trimmed.slice(0, 100));
436
+ const stripped = trimmed.replace(/^[^a-zA-Z]+/, '');
437
+ if (/^thinking\.{0,3}$/i.test(stripped)) {
438
+ // Transient "Thinking..." — just set flag, don't add entry
439
+ thinkingActive = true;
440
+ }
441
+ else if (/^thought for\s/i.test(stripped)) {
442
+ // Completed thinking cycle: "Thought for 1s"
443
+ thinkingActive = false;
444
+ lastThoughtLabel = trimmed;
445
+ progressLog.push({ kind: 'thought', text: trimmed });
446
+ trimProgressLog();
367
447
  }
368
- const elapsed = Math.round((Date.now() - startTime) / 1000);
369
- liveActivityUpdateVersion += 1;
370
- const v = liveActivityUpdateVersion;
371
- upsertLiveActivity(`${PHASE_ICONS.thinking} Process Log`, lastActivityLogText || ACTIVITY_PLACEHOLDER, (0, i18n_1.t)(`⏱️ Elapsed: ${elapsed}s | Process log`), { expectedVersion: v, skipWhenFinalized: true }).catch(() => { });
448
+ else {
449
+ // Thinking content — merge as summary with most recent 'thought' entry
450
+ thinkingContentParts.push(trimmed);
451
+ const firstLine = trimmed.split('\n')[0].trim();
452
+ const heading = firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine;
453
+ // Find most recent thought entry that doesn't yet have content attached
454
+ let merged = false;
455
+ for (let i = progressLog.length - 1; i >= 0; i--) {
456
+ if (progressLog[i].kind === 'thought') {
457
+ // Only merge if no content heading attached yet (no " — ")
458
+ if (!progressLog[i].text.includes(' — ')) {
459
+ progressLog[i].text += ` — ${heading}`;
460
+ merged = true;
461
+ }
462
+ break;
463
+ }
464
+ }
465
+ if (!merged && heading.length > 10) {
466
+ // No thought label to merge into — show as standalone content
467
+ progressLog.push({ kind: 'thought-content', text: heading });
468
+ trimProgressLog();
469
+ }
470
+ }
471
+ triggerProgressRefresh();
372
472
  },
373
473
  onProgress: (text) => {
374
474
  if (isFinalized)
@@ -396,11 +496,11 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
396
496
  const elapsed = Math.round((Date.now() - startTime) / 1000);
397
497
  const isQuotaError = monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected();
398
498
  if (isQuotaError) {
399
- const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
400
499
  liveActivityUpdateVersion += 1;
401
- await upsertLiveActivity(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), { expectedVersion: liveActivityUpdateVersion });
500
+ thinkingActive = false;
501
+ await setProgressMessage(`<b>⚠️ ${(0, telegramFormatter_1.escapeHtml)(modelLabel)} · Quota Reached</b>\n\n${buildProgressBody()}\n\n<i>⏱️ ${elapsed}s</i>`, { expectedVersion: liveActivityUpdateVersion });
402
502
  liveResponseUpdateVersion += 1;
403
- await upsertLiveResponse('⚠️ Model Quota Reached', 'Model quota limit reached. Please wait or switch to a different model.', (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Quota Reached`), { expectedVersion: liveResponseUpdateVersion });
503
+ await upsertLiveResponse('⚠️ Quota Reached', 'Model quota limit reached. Please wait or switch to a different model.', `⏱️ ${elapsed}s`, { expectedVersion: liveResponseUpdateVersion });
404
504
  try {
405
505
  const payload = await (0, modelsUi_1.buildModelsUI)(cdp, () => bridge.quota.fetchQuota());
406
506
  if (payload) {
@@ -412,32 +512,137 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
412
512
  }
413
513
  return;
414
514
  }
415
- const responseText = (finalText && finalText.trim().length > 0) ? finalText : lastProgressText;
416
- const emergencyText = (!responseText || responseText.trim().length === 0) ? await tryEmergencyExtractText() : '';
417
- const finalResponseText = responseText && responseText.trim().length > 0 ? responseText : emergencyText;
418
- const isAlreadyHtml = meta?.source === 'structured';
515
+ // Fresh DOM re-extraction at completion time to ensure we get the
516
+ // complete response polling may have captured partial/stale text.
517
+ let freshText = '';
518
+ let freshIsHtml = false;
519
+ try {
520
+ const contextId = cdp.getPrimaryContextId();
521
+ const evalParams = {
522
+ expression: (0, assistantDomExtractor_1.extractAssistantSegmentsPayloadScript)(),
523
+ returnByValue: true,
524
+ awaitPromise: true,
525
+ };
526
+ if (contextId !== null && contextId !== undefined)
527
+ evalParams.contextId = contextId;
528
+ const freshResult = await cdp.call('Runtime.evaluate', evalParams);
529
+ const freshClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(freshResult?.result?.value);
530
+ if (freshClassified.diagnostics.source === 'dom-structured' && freshClassified.finalOutputText.trim()) {
531
+ freshText = freshClassified.finalOutputText.trim();
532
+ freshIsHtml = true;
533
+ }
534
+ }
535
+ catch (e) {
536
+ logger_1.logger.debug('[onComplete] Fresh structured extraction failed:', e);
537
+ }
538
+ // Pick the best text: fresh extraction > polled finalText > lastProgressText > emergency
539
+ const polledText = (finalText && finalText.trim().length > 0) ? finalText : lastProgressText;
540
+ const bestPolled = polledText && polledText.trim().length > 0 ? polledText : '';
541
+ // Prefer the fresh extraction if it's at least as long (more complete)
542
+ let finalResponseText;
543
+ let isAlreadyHtml;
544
+ if (freshText && freshText.length >= bestPolled.length) {
545
+ finalResponseText = freshText;
546
+ isAlreadyHtml = freshIsHtml;
547
+ }
548
+ else if (bestPolled) {
549
+ finalResponseText = bestPolled;
550
+ isAlreadyHtml = meta?.source === 'structured';
551
+ }
552
+ else {
553
+ const emergencyText = await tryEmergencyExtractText();
554
+ finalResponseText = emergencyText;
555
+ isAlreadyHtml = false;
556
+ }
419
557
  const separated = isAlreadyHtml ? { output: finalResponseText, logs: '' } : (0, telegramFormatter_1.splitOutputAndLogs)(finalResponseText);
420
558
  const finalOutputText = separated.output || finalResponseText;
421
- const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
422
- if (finalLogText && finalLogText.trim().length > 0) {
423
- logger_1.logger.divider('Process Log');
424
- console.info(finalLogText);
559
+ // Send collapsible thinking block as a separate message before the response.
560
+ // Extract both label and content directly from DOM at completion time,
561
+ // so we don't depend on polling (2s interval) having captured thinking events.
562
+ try {
563
+ const thinkExtract = await cdp.call('Runtime.evaluate', {
564
+ expression: `(function() {
565
+ var panel = document.querySelector('.antigravity-agent-side-panel');
566
+ var scope = panel || document;
567
+ var details = scope.querySelectorAll('details');
568
+ var blocks = [];
569
+ for (var i = 0; i < details.length; i++) {
570
+ var d = details[i];
571
+ var summary = d.querySelector('summary');
572
+ if (!summary) continue;
573
+ var rawLabel = (summary.textContent || '').trim();
574
+ var stripped = rawLabel.replace(/^[^a-zA-Z]+/, '');
575
+ if (!/^(?:thought for|thinking)\\b/i.test(stripped)) continue;
576
+ var wasOpen = d.open;
577
+ if (!wasOpen) d.open = true;
578
+ // Try children first, then fall back to full textContent minus summary
579
+ var children = d.children;
580
+ var parts = [];
581
+ for (var c = 0; c < children.length; c++) {
582
+ if (children[c].tagName === 'SUMMARY' || children[c].tagName === 'STYLE') continue;
583
+ var t = (children[c].innerText || children[c].textContent || '').trim();
584
+ if (t && t.length >= 5) parts.push(t);
585
+ }
586
+ // Fallback: use detail's full text minus the summary text
587
+ if (parts.length === 0) {
588
+ var fullText = (d.innerText || d.textContent || '').trim();
589
+ var bodyText = fullText.replace(rawLabel, '').trim();
590
+ if (bodyText && bodyText.length >= 5) parts.push(bodyText);
591
+ }
592
+ if (!wasOpen) d.open = false;
593
+ blocks.push({ label: rawLabel, body: parts.join('\\n\\n') });
594
+ }
595
+ return blocks;
596
+ })()`,
597
+ returnByValue: true,
598
+ });
599
+ const thinkBlocks = Array.isArray(thinkExtract?.result?.value) ? thinkExtract.result.value : [];
600
+ if (thinkBlocks.length > 0) {
601
+ // Also incorporate poll-accumulated content if available
602
+ const accumulatedBody = thinkingContentParts.join('\n\n');
603
+ // Build combined thinking message — merge all blocks
604
+ const sections = [];
605
+ for (const block of thinkBlocks) {
606
+ const label = block.label || lastThoughtLabel || 'Thinking';
607
+ const body = block.body || accumulatedBody || '';
608
+ if (body) {
609
+ sections.push(` 💭 <b>${(0, telegramFormatter_1.escapeHtml)(label)}</b>\n\n<i>${(0, telegramFormatter_1.escapeHtml)(body)}</i>`);
610
+ }
611
+ else {
612
+ sections.push(` 💭 <b>${(0, telegramFormatter_1.escapeHtml)(label)}</b>`);
613
+ }
614
+ }
615
+ const combined = sections.join('\n\n');
616
+ const maxThinkLen = TELEGRAM_MSG_LIMIT - 100;
617
+ const trimmed = combined.length > maxThinkLen ? combined.slice(0, maxThinkLen) + '...' : combined;
618
+ const thinkMsg = `<blockquote expandable>${trimmed}</blockquote>`;
619
+ logger_1.logger.info(`[Bot] Sending thinking block: ${thinkBlocks.length} block(s), ${combined.length} chars`);
620
+ await sendMsg(thinkMsg);
621
+ }
622
+ else {
623
+ logger_1.logger.info('[Bot] No thinking blocks found in DOM at completion time');
624
+ }
625
+ }
626
+ catch (e) {
627
+ logger_1.logger.error('[Bot] Failed to send thinking block:', e);
425
628
  }
426
629
  if (finalOutputText && finalOutputText.trim().length > 0) {
427
630
  logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
428
631
  console.info(finalOutputText);
429
632
  }
430
633
  logger_1.logger.divider();
634
+ // Compact progress message: show completed title + event log
431
635
  liveActivityUpdateVersion += 1;
432
- await upsertLiveActivity(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), { expectedVersion: liveActivityUpdateVersion });
636
+ thinkingActive = false;
637
+ const completedBody = buildProgressBody();
638
+ await setProgressMessage(`<b>${PHASE_ICONS.complete} ${(0, telegramFormatter_1.escapeHtml)(modelLabel)} · ${elapsed}s</b>\n\n${completedBody}`, { expectedVersion: liveActivityUpdateVersion });
433
639
  liveResponseUpdateVersion += 1;
434
640
  if (finalOutputText && finalOutputText.trim().length > 0) {
435
- const title = `${PHASE_ICONS.complete} Final Output`;
436
- const footer = (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`);
437
- await sendChunkedResponse(title, footer, finalOutputText, isAlreadyHtml);
641
+ const footer = `⏱️ ${elapsed}s`;
642
+ await sendChunkedResponse('', footer, finalOutputText, isAlreadyHtml);
438
643
  }
439
644
  else {
440
- await upsertLiveResponse(`${PHASE_ICONS.complete} Complete`, (0, i18n_1.t)('Failed to extract response. Use /screenshot to verify.'), (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), { expectedVersion: liveResponseUpdateVersion });
645
+ await upsertLiveResponse(`${PHASE_ICONS.complete} Complete`, (0, i18n_1.t)('Failed to extract response. Use /screenshot to verify.'), `⏱️ ${elapsed}s`, { expectedVersion: liveResponseUpdateVersion });
441
646
  }
442
647
  if (options) {
443
648
  try {
@@ -489,16 +694,14 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
489
694
  const timeoutText = (lastText && lastText.trim().length > 0) ? lastText : lastProgressText;
490
695
  const timeoutIsHtml = monitor.getLastExtractionSource() === 'structured';
491
696
  const separated = timeoutIsHtml ? { output: timeoutText || '', logs: '' } : (0, telegramFormatter_1.splitOutputAndLogs)(timeoutText || '');
492
- const sanitizedTimeoutLogs = lastActivityLogText || processLogBuffer.snapshot();
493
697
  const payload = separated.output && separated.output.trim().length > 0
494
698
  ? `${separated.output}\n\n[Monitor Ended] Timeout after 30 minutes.`
495
699
  : 'Monitor ended after 30 minutes. No text was retrieved.';
496
700
  liveResponseUpdateVersion += 1;
497
- const timeoutTitle = `${PHASE_ICONS.timeout} Timeout`;
498
- const timeoutFooter = `⏱️ Elapsed: ${elapsed}s | Timeout`;
499
- await sendChunkedResponse(timeoutTitle, timeoutFooter, payload, timeoutIsHtml);
701
+ await sendChunkedResponse(`${PHASE_ICONS.timeout} Timeout`, `⏱️ ${elapsed}s`, payload, timeoutIsHtml);
500
702
  liveActivityUpdateVersion += 1;
501
- await upsertLiveActivity(`${PHASE_ICONS.thinking} Process Log`, sanitizedTimeoutLogs || ACTIVITY_PLACEHOLDER, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), { expectedVersion: liveActivityUpdateVersion });
703
+ thinkingActive = false;
704
+ await setProgressMessage(`<b>${PHASE_ICONS.timeout} ${(0, telegramFormatter_1.escapeHtml)(modelLabel)} · ${elapsed}s</b>\n\n${buildProgressBody()}`, { expectedVersion: liveActivityUpdateVersion });
502
705
  }
503
706
  catch (error) {
504
707
  logger_1.logger.error(`[sendPrompt:${monitorTraceId}] onTimeout failed:`, error);
@@ -511,10 +714,7 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
511
714
  clearInterval(elapsedTimer);
512
715
  return;
513
716
  }
514
- const elapsed = Math.round((Date.now() - startTime) / 1000);
515
- liveActivityUpdateVersion += 1;
516
- const v = liveActivityUpdateVersion;
517
- upsertLiveActivity(`${PHASE_ICONS.thinking} Process Log`, lastActivityLogText || ACTIVITY_PLACEHOLDER, (0, i18n_1.t)(`⏱️ Elapsed: ${elapsed}s | Process log`), { expectedVersion: v, skipWhenFinalized: true }).catch(() => { });
717
+ triggerProgressRefresh();
518
718
  }, 5000);
519
719
  }
520
720
  catch (e) {
@@ -559,6 +759,34 @@ const startBot = async (cliLogLevel) => {
559
759
  const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
560
760
  const bot = new grammy_1.Bot(config.telegramBotToken);
561
761
  bridge.botApi = bot.api;
762
+ // Notify user on WebSocket connection lifecycle events
763
+ bridge.pool.on('workspace:disconnected', (projectName) => {
764
+ const channel = bridge.lastActiveChannel;
765
+ if (!channel || !bridge.botApi)
766
+ return;
767
+ bridge.botApi.sendMessage(channel.chatId, `⚠️ <b>${(0, telegramFormatter_1.escapeHtml)(projectName)}</b>: Connection lost. Reconnecting…`, {
768
+ parse_mode: 'HTML',
769
+ message_thread_id: channel.threadId,
770
+ }).catch((err) => logger_1.logger.error('[Bot] Failed to send disconnect notification:', err));
771
+ });
772
+ bridge.pool.on('workspace:reconnected', (projectName) => {
773
+ const channel = bridge.lastActiveChannel;
774
+ if (!channel || !bridge.botApi)
775
+ return;
776
+ bridge.botApi.sendMessage(channel.chatId, `✅ <b>${(0, telegramFormatter_1.escapeHtml)(projectName)}</b>: Reconnected.`, {
777
+ parse_mode: 'HTML',
778
+ message_thread_id: channel.threadId,
779
+ }).catch((err) => logger_1.logger.error('[Bot] Failed to send reconnect notification:', err));
780
+ });
781
+ bridge.pool.on('workspace:reconnectFailed', (projectName) => {
782
+ const channel = bridge.lastActiveChannel;
783
+ if (!channel || !bridge.botApi)
784
+ return;
785
+ bridge.botApi.sendMessage(channel.chatId, `❌ <b>${(0, telegramFormatter_1.escapeHtml)(projectName)}</b>: Reconnection failed. Send a message to retry.`, {
786
+ parse_mode: 'HTML',
787
+ message_thread_id: channel.threadId,
788
+ }).catch((err) => logger_1.logger.error('[Bot] Failed to send reconnect-failed notification:', err));
789
+ });
562
790
  const topicManager = new telegramTopicManager_1.TelegramTopicManager(bot.api, 0);
563
791
  // Auth middleware
564
792
  bot.use(async (ctx, next) => {
@@ -645,11 +873,14 @@ const startBot = async (cliLogLevel) => {
645
873
  });
646
874
  // /model command
647
875
  bot.command('model', async (ctx) => {
876
+ const ch = getChannel(ctx);
877
+ const resolved = await resolveWorkspaceAndCdp(ch);
878
+ const getCdp = () => resolved?.cdp ?? (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
648
879
  const modelName = ctx.match?.trim();
649
880
  if (modelName) {
650
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
881
+ const cdp = getCdp();
651
882
  if (!cdp) {
652
- await ctx.reply('Not connected to CDP.');
883
+ await ctx.reply('Not connected to CDP. Send a message first to connect.');
653
884
  return;
654
885
  }
655
886
  const res = await cdp.setUiModel(modelName);
@@ -661,7 +892,7 @@ const startBot = async (cliLogLevel) => {
661
892
  }
662
893
  }
663
894
  else {
664
- await (0, modelsUi_1.sendModelsUI)(async (text, keyboard) => { await replyHtml(ctx, text, keyboard); }, { getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge), fetchQuota: async () => bridge.quota.fetchQuota() });
895
+ await (0, modelsUi_1.sendModelsUI)(async (text, keyboard) => { await replyHtml(ctx, text, keyboard); }, { getCurrentCdp: getCdp, fetchQuota: async () => bridge.quota.fetchQuota() });
665
896
  }
666
897
  });
667
898
  // /template command