remoat 0.2.8 โ†’ 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");
@@ -39,6 +39,7 @@ const autoAcceptUi_1 = require("../ui/autoAcceptUi");
39
39
  const screenshotUi_1 = require("../ui/screenshotUi");
40
40
  const projectListUi_1 = require("../ui/projectListUi");
41
41
  const sessionPickerUi_1 = require("../ui/sessionPickerUi");
42
+ const planUi_1 = require("../ui/planUi");
42
43
  const PHASE_ICONS = {
43
44
  sending: '๐Ÿ“ก',
44
45
  thinking: '๐Ÿง ',
@@ -68,7 +69,7 @@ function stripHtmlForFile(html) {
68
69
  // Links
69
70
  text = text.replace(/<a\s+href="([^"]*)">([\s\S]*?)<\/a>/gi, '[$2]($1)');
70
71
  // Blockquotes
71
- 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'));
72
73
  // Strip remaining tags
73
74
  text = text.replace(/<[^>]+>/g, '');
74
75
  // Decode entities
@@ -78,6 +79,10 @@ function stripHtmlForFile(html) {
78
79
  return text;
79
80
  }
80
81
  const userStopRequestedChannels = new Set();
82
+ /** Channels where the user is expected to type plan edit instructions */
83
+ const planEditPendingChannels = new Map();
84
+ /** Cached plan content pages per channel */
85
+ const planContentCache = new Map();
81
86
  function channelKey(ch) {
82
87
  return ch.threadId ? `${ch.chatId}:${ch.threadId}` : String(ch.chatId);
83
88
  }
@@ -137,7 +142,9 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
137
142
  /** Send a potentially long response, splitting into chunks and attaching a .md file if needed. */
138
143
  const sendChunkedResponse = async (title, footer, rawBody, isAlreadyHtml) => {
139
144
  const formattedBody = isAlreadyHtml ? rawBody : (0, telegramFormatter_1.formatForTelegram)(rawBody);
140
- 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}`;
141
148
  if (fullMsg.length <= TELEGRAM_MSG_LIMIT) {
142
149
  await upsertLiveResponse(title, rawBody, footer, { expectedVersion: liveResponseUpdateVersion, isAlreadyHtml, skipTruncation: true });
143
150
  return;
@@ -149,10 +156,12 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
149
156
  for (let pi = 0; pi < inlineCount; pi++) {
150
157
  const partLabel = hasFile ? `(${pi + 1}/${inlineCount}+file)` : `(${pi + 1}/${total})`;
151
158
  if (pi === 0) {
152
- 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 });
153
161
  }
154
162
  else {
155
- 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>`);
156
165
  }
157
166
  }
158
167
  if (hasFile) {
@@ -177,43 +186,102 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
177
186
  const localMode = modeService.getCurrentMode();
178
187
  const modeName = modeService_1.MODE_UI_NAMES[localMode] || localMode;
179
188
  const currentModel = (await cdp.getCurrentModel()) || modelService.getCurrentModel();
180
- 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
+ }
181
200
  let isFinalized = false;
182
201
  let elapsedTimer = null;
183
202
  let lastProgressText = '';
184
- let lastActivityLogText = '';
185
203
  const LIVE_RESPONSE_MAX_LEN = 3800;
186
- const LIVE_ACTIVITY_MAX_LEN = 3800;
187
- 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;
188
206
  let liveResponseMsgId = null;
189
- let liveActivityMsgId = null;
190
207
  let lastLiveResponseKey = '';
191
208
  let lastLiveActivityKey = '';
192
209
  let liveResponseUpdateVersion = 0;
193
210
  let liveActivityUpdateVersion = 0;
194
- 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
+ };
195
274
  const buildLiveResponseText = (title, rawText, footer, isAlreadyHtml = false, skipTruncation = false) => {
196
275
  const normalized = (rawText || '').trim();
197
276
  const body = normalized
198
277
  ? (isAlreadyHtml ? normalized : (0, telegramFormatter_1.formatForTelegram)(normalized))
199
- : (0, i18n_1.t)('Waiting for output...');
278
+ : (0, i18n_1.t)('Generating...');
200
279
  const truncated = (!skipTruncation && body.length > LIVE_RESPONSE_MAX_LEN)
201
280
  ? '...(beginning truncated)\n' + body.slice(-LIVE_RESPONSE_MAX_LEN + 30)
202
281
  : body;
203
- return `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n${truncated}\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>`;
204
- };
205
- const buildLiveActivityText = (title, rawText, footer) => {
206
- const normalized = (rawText || '').trim();
207
- const body = normalized
208
- ? (0, streamMessageFormatter_1.fitForSingleEmbedDescription)((0, telegramFormatter_1.formatForTelegram)(normalized), LIVE_ACTIVITY_MAX_LEN)
209
- : ACTIVITY_PLACEHOLDER;
210
- return `<b>${(0, telegramFormatter_1.escapeHtml)(title)}</b>\n\n${body}\n\n<i>${(0, telegramFormatter_1.escapeHtml)(footer)}</i>`;
211
- };
212
- const appendProcessLogs = (text) => {
213
- const normalized = (text || '').trim();
214
- if (!normalized)
215
- return processLogBuffer.snapshot();
216
- 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}`;
217
285
  };
218
286
  const upsertLiveResponse = (title, rawText, footer, opts) => enqueueResponse(async () => {
219
287
  if (opts?.skipWhenFinalized && isFinalized)
@@ -232,16 +300,18 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
232
300
  liveResponseMsgId = await sendMsg(text);
233
301
  }
234
302
  }, 'upsert-response');
235
- 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 () => {
236
305
  if (opts?.skipWhenFinalized && isFinalized)
237
306
  return;
238
307
  if (opts?.expectedVersion !== undefined && opts.expectedVersion !== liveActivityUpdateVersion)
239
308
  return;
240
- const text = buildLiveActivityText(title, rawText, footer);
241
- const renderKey = `${title}|${rawText.slice(0, 200)}|${footer}`;
242
- 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)
243
313
  return;
244
- lastLiveActivityKey = renderKey;
314
+ lastLiveActivityKey = bodySnap;
245
315
  if (liveActivityMsgId) {
246
316
  await editMsg(liveActivityMsgId, text);
247
317
  }
@@ -249,6 +319,18 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
249
319
  liveActivityMsgId = await sendMsg(text);
250
320
  }
251
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');
252
334
  const sendGeneratedImages = async (responseText) => {
253
335
  const imageIntentPattern = /(image|images|png|jpg|jpeg|gif|webp|illustration|diagram|render)/i;
254
336
  const imageUrlPattern = /https?:\/\/\S+\.(png|jpg|jpeg|gif|webp)/i;
@@ -316,7 +398,15 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
316
398
  return;
317
399
  }
318
400
  const startTime = Date.now();
319
- 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());
320
410
  monitor = new responseMonitor_1.ResponseMonitor({
321
411
  cdpService: cdp,
322
412
  pollIntervalMs: 2000,
@@ -326,12 +416,59 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
326
416
  onProcessLog: (logText) => {
327
417
  if (isFinalized)
328
418
  return;
329
- if (logText && logText.trim().length > 0)
330
- lastActivityLogText = appendProcessLogs(logText);
331
- const elapsed = Math.round((Date.now() - startTime) / 1000);
332
- liveActivityUpdateVersion += 1;
333
- const v = liveActivityUpdateVersion;
334
- 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
+ }
428
+ },
429
+ onThinkingLog: (thinkingText) => {
430
+ if (isFinalized)
431
+ return;
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();
447
+ }
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();
335
472
  },
336
473
  onProgress: (text) => {
337
474
  if (isFinalized)
@@ -342,6 +479,8 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
342
479
  lastProgressText = separated.output;
343
480
  },
344
481
  onComplete: async (finalText, meta) => {
482
+ if (isFinalized)
483
+ return; // Guard: prevent duplicate completion
345
484
  isFinalized = true;
346
485
  if (elapsedTimer) {
347
486
  clearInterval(elapsedTimer);
@@ -357,11 +496,11 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
357
496
  const elapsed = Math.round((Date.now() - startTime) / 1000);
358
497
  const isQuotaError = monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected();
359
498
  if (isQuotaError) {
360
- const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
361
499
  liveActivityUpdateVersion += 1;
362
- 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 });
363
502
  liveResponseUpdateVersion += 1;
364
- 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 });
365
504
  try {
366
505
  const payload = await (0, modelsUi_1.buildModelsUI)(cdp, () => bridge.quota.fetchQuota());
367
506
  if (payload) {
@@ -373,32 +512,137 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
373
512
  }
374
513
  return;
375
514
  }
376
- const responseText = (finalText && finalText.trim().length > 0) ? finalText : lastProgressText;
377
- const emergencyText = (!responseText || responseText.trim().length === 0) ? await tryEmergencyExtractText() : '';
378
- const finalResponseText = responseText && responseText.trim().length > 0 ? responseText : emergencyText;
379
- 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
+ }
380
557
  const separated = isAlreadyHtml ? { output: finalResponseText, logs: '' } : (0, telegramFormatter_1.splitOutputAndLogs)(finalResponseText);
381
558
  const finalOutputText = separated.output || finalResponseText;
382
- const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
383
- if (finalLogText && finalLogText.trim().length > 0) {
384
- logger_1.logger.divider('Process Log');
385
- 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);
386
628
  }
387
629
  if (finalOutputText && finalOutputText.trim().length > 0) {
388
630
  logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
389
631
  console.info(finalOutputText);
390
632
  }
391
633
  logger_1.logger.divider();
634
+ // Compact progress message: show completed title + event log
392
635
  liveActivityUpdateVersion += 1;
393
- 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 });
394
639
  liveResponseUpdateVersion += 1;
395
640
  if (finalOutputText && finalOutputText.trim().length > 0) {
396
- const title = `${PHASE_ICONS.complete} Final Output`;
397
- const footer = (0, i18n_1.t)(`โฑ๏ธ Time: ${elapsed}s | Complete`);
398
- await sendChunkedResponse(title, footer, finalOutputText, isAlreadyHtml);
641
+ const footer = `โฑ๏ธ ${elapsed}s`;
642
+ await sendChunkedResponse('', footer, finalOutputText, isAlreadyHtml);
399
643
  }
400
644
  else {
401
- 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 });
402
646
  }
403
647
  if (options) {
404
648
  try {
@@ -450,16 +694,14 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
450
694
  const timeoutText = (lastText && lastText.trim().length > 0) ? lastText : lastProgressText;
451
695
  const timeoutIsHtml = monitor.getLastExtractionSource() === 'structured';
452
696
  const separated = timeoutIsHtml ? { output: timeoutText || '', logs: '' } : (0, telegramFormatter_1.splitOutputAndLogs)(timeoutText || '');
453
- const sanitizedTimeoutLogs = lastActivityLogText || processLogBuffer.snapshot();
454
697
  const payload = separated.output && separated.output.trim().length > 0
455
698
  ? `${separated.output}\n\n[Monitor Ended] Timeout after 30 minutes.`
456
699
  : 'Monitor ended after 30 minutes. No text was retrieved.';
457
700
  liveResponseUpdateVersion += 1;
458
- const timeoutTitle = `${PHASE_ICONS.timeout} Timeout`;
459
- const timeoutFooter = `โฑ๏ธ Elapsed: ${elapsed}s | Timeout`;
460
- await sendChunkedResponse(timeoutTitle, timeoutFooter, payload, timeoutIsHtml);
701
+ await sendChunkedResponse(`${PHASE_ICONS.timeout} Timeout`, `โฑ๏ธ ${elapsed}s`, payload, timeoutIsHtml);
461
702
  liveActivityUpdateVersion += 1;
462
- 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 });
463
705
  }
464
706
  catch (error) {
465
707
  logger_1.logger.error(`[sendPrompt:${monitorTraceId}] onTimeout failed:`, error);
@@ -472,10 +714,7 @@ async function sendPromptToAntigravity(bridge, channel, prompt, cdp, modeService
472
714
  clearInterval(elapsedTimer);
473
715
  return;
474
716
  }
475
- const elapsed = Math.round((Date.now() - startTime) / 1000);
476
- liveActivityUpdateVersion += 1;
477
- const v = liveActivityUpdateVersion;
478
- 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();
479
718
  }, 5000);
480
719
  }
481
720
  catch (e) {
@@ -520,6 +759,34 @@ const startBot = async (cliLogLevel) => {
520
759
  const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
521
760
  const bot = new grammy_1.Bot(config.telegramBotToken);
522
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
+ });
523
790
  const topicManager = new telegramTopicManager_1.TelegramTopicManager(bot.api, 0);
524
791
  // Auth middleware
525
792
  bot.use(async (ctx, next) => {
@@ -606,11 +873,14 @@ const startBot = async (cliLogLevel) => {
606
873
  });
607
874
  // /model command
608
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);
609
879
  const modelName = ctx.match?.trim();
610
880
  if (modelName) {
611
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
881
+ const cdp = getCdp();
612
882
  if (!cdp) {
613
- await ctx.reply('Not connected to CDP.');
883
+ await ctx.reply('Not connected to CDP. Send a message first to connect.');
614
884
  return;
615
885
  }
616
886
  const res = await cdp.setUiModel(modelName);
@@ -622,7 +892,7 @@ const startBot = async (cliLogLevel) => {
622
892
  }
623
893
  }
624
894
  else {
625
- 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() });
626
896
  }
627
897
  });
628
898
  // /template command
@@ -1051,7 +1321,7 @@ const startBot = async (cliLogLevel) => {
1051
1321
  }
1052
1322
  return;
1053
1323
  }
1054
- // Planning buttons
1324
+ // Planning buttons (legacy parsing for backward compat)
1055
1325
  const planningAction = (0, cdpBridgeManager_1.parsePlanningCustomId)(data);
1056
1326
  if (planningAction) {
1057
1327
  const projectName = planningAction.projectName ?? bridge.lastActiveWorkspace;
@@ -1072,8 +1342,12 @@ const startBot = async (cliLogLevel) => {
1072
1342
  await new Promise(r => setTimeout(r, 500));
1073
1343
  }
1074
1344
  if (planContent) {
1075
- const truncated = planContent.length > 3800 ? planContent.substring(0, 3800) + '\n\n(truncated)' : planContent;
1076
- await bot.api.sendMessage(ch.chatId, `<b>Plan Content</b>\n\n${(0, telegramFormatter_1.escapeHtml)(truncated)}`, { parse_mode: 'HTML', message_thread_id: ch.threadId });
1345
+ const chKey = channelKey(ch);
1346
+ const pages = (0, planUi_1.paginatePlanContent)(planContent);
1347
+ planContentCache.set(chKey, pages);
1348
+ const targetChannelStr = ch.threadId ? String(ch.threadId) : String(ch.chatId);
1349
+ const { text: pageText, keyboard: pageKeyboard } = (0, planUi_1.buildPlanContentUI)(pages, 0, projectName || '', targetChannelStr);
1350
+ await bot.api.sendMessage(ch.chatId, pageText, { parse_mode: 'HTML', message_thread_id: ch.threadId, reply_markup: pageKeyboard });
1077
1351
  }
1078
1352
  }
1079
1353
  await ctx.answerCallbackQuery({ text: clicked ? 'Opened' : 'Open button not found.' });
@@ -1089,6 +1363,104 @@ const startBot = async (cliLogLevel) => {
1089
1363
  }
1090
1364
  return;
1091
1365
  }
1366
+ // New plan UI buttons (View/Proceed/Edit/Refresh)
1367
+ if (data.startsWith(planUi_1.PLAN_VIEW_BTN + ':')) {
1368
+ const suffix = data.substring(planUi_1.PLAN_VIEW_BTN.length + 1);
1369
+ const [projectName] = suffix.split(':');
1370
+ const detector = projectName ? bridge.pool.getPlanningDetector(projectName) : undefined;
1371
+ if (!detector) {
1372
+ await ctx.answerCallbackQuery({ text: 'Planning detector not found.' });
1373
+ return;
1374
+ }
1375
+ const clicked = await detector.clickOpenButton();
1376
+ if (clicked) {
1377
+ await new Promise(r => setTimeout(r, 500));
1378
+ let planContent = null;
1379
+ for (let attempt = 0; attempt < 3; attempt++) {
1380
+ planContent = await detector.extractPlanContent();
1381
+ if (planContent)
1382
+ break;
1383
+ await new Promise(r => setTimeout(r, 500));
1384
+ }
1385
+ if (planContent) {
1386
+ const chKey = channelKey(ch);
1387
+ const pages = (0, planUi_1.paginatePlanContent)(planContent);
1388
+ planContentCache.set(chKey, pages);
1389
+ const targetChannelStr = ch.threadId ? String(ch.threadId) : String(ch.chatId);
1390
+ const { text: pageText, keyboard: pageKeyboard } = (0, planUi_1.buildPlanContentUI)(pages, 0, projectName, targetChannelStr);
1391
+ await bot.api.sendMessage(ch.chatId, pageText, { parse_mode: 'HTML', message_thread_id: ch.threadId, reply_markup: pageKeyboard });
1392
+ }
1393
+ }
1394
+ await ctx.answerCallbackQuery({ text: clicked ? 'Opened' : 'Open button not found.' });
1395
+ return;
1396
+ }
1397
+ if (data.startsWith(planUi_1.PLAN_PROCEED_BTN + ':')) {
1398
+ const suffix = data.substring(planUi_1.PLAN_PROCEED_BTN.length + 1);
1399
+ const [projectName] = suffix.split(':');
1400
+ const detector = projectName ? bridge.pool.getPlanningDetector(projectName) : undefined;
1401
+ if (!detector) {
1402
+ await ctx.answerCallbackQuery({ text: 'Planning detector not found.' });
1403
+ return;
1404
+ }
1405
+ const clicked = await detector.clickProceedButton();
1406
+ if (clicked) {
1407
+ planEditPendingChannels.delete(channelKey(ch));
1408
+ try {
1409
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined });
1410
+ }
1411
+ catch { }
1412
+ }
1413
+ await ctx.answerCallbackQuery({ text: clicked ? 'Proceeding...' : 'Proceed button not found.' });
1414
+ return;
1415
+ }
1416
+ if (data.startsWith(planUi_1.PLAN_EDIT_BTN + ':')) {
1417
+ const suffix = data.substring(planUi_1.PLAN_EDIT_BTN.length + 1);
1418
+ const [projectName] = suffix.split(':');
1419
+ planEditPendingChannels.set(channelKey(ch), { projectName });
1420
+ await ctx.answerCallbackQuery({ text: 'Type your edit instructions (or /cancel).' });
1421
+ await bot.api.sendMessage(ch.chatId, '<b>Edit Plan</b>\n\nType your plan edit instructions below.\nSend <code>/cancel</code> to cancel.', { parse_mode: 'HTML', message_thread_id: ch.threadId });
1422
+ return;
1423
+ }
1424
+ if (data.startsWith(planUi_1.PLAN_REFRESH_BTN + ':')) {
1425
+ const suffix = data.substring(planUi_1.PLAN_REFRESH_BTN.length + 1);
1426
+ const [projectName, targetChannelStr] = suffix.split(':');
1427
+ const detector = projectName ? bridge.pool.getPlanningDetector(projectName) : undefined;
1428
+ if (!detector) {
1429
+ await ctx.answerCallbackQuery({ text: 'Planning detector not found.' });
1430
+ return;
1431
+ }
1432
+ const info = detector.getLastDetectedInfo();
1433
+ if (info) {
1434
+ const { text: uiText, keyboard: uiKeyboard } = (0, planUi_1.buildPlanNotificationUI)(info, projectName, targetChannelStr || String(ch.chatId));
1435
+ try {
1436
+ await ctx.editMessageText(uiText, { parse_mode: 'HTML', reply_markup: uiKeyboard });
1437
+ }
1438
+ catch { }
1439
+ }
1440
+ await ctx.answerCallbackQuery({ text: 'Refreshed' });
1441
+ return;
1442
+ }
1443
+ // Plan pagination
1444
+ if (data.startsWith(planUi_1.PLAN_PAGE_PREFIX + ':')) {
1445
+ const rest = data.substring(planUi_1.PLAN_PAGE_PREFIX.length + 1);
1446
+ const colonIdx = rest.indexOf(':');
1447
+ const page = parseInt(rest.substring(0, colonIdx), 10);
1448
+ const suffix = rest.substring(colonIdx + 1);
1449
+ const [projectName, targetChannelStr] = suffix.split(':');
1450
+ const chKey = channelKey(ch);
1451
+ const pages = planContentCache.get(chKey);
1452
+ if (!pages || isNaN(page)) {
1453
+ await ctx.answerCallbackQuery({ text: 'Page not found.' });
1454
+ return;
1455
+ }
1456
+ const { text: pageText, keyboard: pageKeyboard } = (0, planUi_1.buildPlanContentUI)(pages, page, projectName, targetChannelStr || String(ch.chatId));
1457
+ try {
1458
+ await ctx.editMessageText(pageText, { parse_mode: 'HTML', reply_markup: pageKeyboard });
1459
+ }
1460
+ catch { }
1461
+ await ctx.answerCallbackQuery({ text: `Page ${page + 1}/${pages.length}` });
1462
+ return;
1463
+ }
1092
1464
  // Error popup buttons
1093
1465
  const errorAction = (0, cdpBridgeManager_1.parseErrorPopupCustomId)(data);
1094
1466
  if (errorAction) {
@@ -1187,6 +1559,32 @@ const startBot = async (cliLogLevel) => {
1187
1559
  const text = ctx.message.text.trim();
1188
1560
  if (!text)
1189
1561
  return;
1562
+ // Plan edit interception
1563
+ const pendingPlanEdit = planEditPendingChannels.get(key);
1564
+ if (pendingPlanEdit) {
1565
+ if (text === '/cancel') {
1566
+ planEditPendingChannels.delete(key);
1567
+ await ctx.reply('Plan edit cancelled.');
1568
+ return;
1569
+ }
1570
+ planEditPendingChannels.delete(key);
1571
+ const editPrompt = `Please revise the plan based on the following feedback:\n\n${text}`;
1572
+ const resolved = await resolveWorkspaceAndCdp(ch);
1573
+ const cdp = resolved?.cdp ?? (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1574
+ if (!cdp) {
1575
+ await ctx.reply('Not connected to CDP.');
1576
+ return;
1577
+ }
1578
+ await ctx.reply('Sending plan edit...');
1579
+ await promptDispatcher.send({
1580
+ channel: ch,
1581
+ prompt: editPrompt,
1582
+ cdp,
1583
+ inboundImages: [],
1584
+ options: { chatSessionService, chatSessionRepo, topicManager, titleGenerator },
1585
+ });
1586
+ return;
1587
+ }
1190
1588
  // Check if it looks like a text command
1191
1589
  const parsed = (0, messageParser_1.parseMessageContent)(text);
1192
1590
  if (parsed.isCommand && parsed.commandName) {