lazy-gravity 0.0.4 → 0.2.0

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 (44) hide show
  1. package/README.md +22 -7
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +25 -19
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +445 -126
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +40 -0
  9. package/dist/commands/workspaceCommandHandler.js +17 -28
  10. package/dist/database/chatSessionRepository.js +10 -0
  11. package/dist/database/userPreferenceRepository.js +72 -0
  12. package/dist/events/interactionCreateHandler.js +338 -30
  13. package/dist/events/messageCreateHandler.js +161 -47
  14. package/dist/services/antigravityLauncher.js +4 -3
  15. package/dist/services/approvalDetector.js +7 -0
  16. package/dist/services/assistantDomExtractor.js +339 -0
  17. package/dist/services/cdpBridgeManager.js +323 -39
  18. package/dist/services/cdpConnectionPool.js +117 -33
  19. package/dist/services/cdpService.js +149 -53
  20. package/dist/services/chatSessionService.js +229 -8
  21. package/dist/services/errorPopupDetector.js +271 -0
  22. package/dist/services/planningDetector.js +318 -0
  23. package/dist/services/responseMonitor.js +308 -70
  24. package/dist/services/retryStore.js +46 -0
  25. package/dist/services/updateCheckService.js +147 -0
  26. package/dist/services/userMessageDetector.js +221 -0
  27. package/dist/ui/buttonUtils.js +33 -0
  28. package/dist/ui/modeUi.js +11 -1
  29. package/dist/ui/modelsUi.js +24 -13
  30. package/dist/ui/outputUi.js +30 -0
  31. package/dist/ui/projectListUi.js +83 -0
  32. package/dist/ui/sessionPickerUi.js +48 -0
  33. package/dist/utils/antigravityPaths.js +94 -0
  34. package/dist/utils/configLoader.js +18 -0
  35. package/dist/utils/discordButtonUtils.js +33 -0
  36. package/dist/utils/discordFormatter.js +149 -16
  37. package/dist/utils/htmlToDiscordMarkdown.js +184 -0
  38. package/dist/utils/logBuffer.js +47 -0
  39. package/dist/utils/logFileTransport.js +147 -0
  40. package/dist/utils/logger.js +86 -21
  41. package/dist/utils/pathUtils.js +57 -0
  42. package/dist/utils/plainTextFormatter.js +70 -0
  43. package/dist/utils/processLogBuffer.js +4 -0
  44. package/package.json +4 -4
package/dist/bot/index.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -7,6 +40,7 @@ exports.startBot = exports.getResponseDeliveryModeForTest = void 0;
7
40
  exports.createSerialTaskQueueForTest = createSerialTaskQueueForTest;
8
41
  const i18n_1 = require("../utils/i18n");
9
42
  const logger_1 = require("../utils/logger");
43
+ const logBuffer_1 = require("../utils/logBuffer");
10
44
  const discord_js_1 = require("discord.js");
11
45
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
12
46
  const config_1 = require("../utils/config");
@@ -23,9 +57,11 @@ const chatCommandHandler_1 = require("../commands/chatCommandHandler");
23
57
  const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
24
58
  const channelManager_1 = require("../services/channelManager");
25
59
  const titleGeneratorService_1 = require("../services/titleGeneratorService");
60
+ const joinCommandHandler_1 = require("../commands/joinCommandHandler");
26
61
  const chatSessionService_1 = require("../services/chatSessionService");
27
62
  const responseMonitor_1 = require("../services/responseMonitor");
28
63
  const antigravityLauncher_1 = require("../services/antigravityLauncher");
64
+ const pathUtils_1 = require("../utils/pathUtils");
29
65
  const promptDispatcher_1 = require("../services/promptDispatcher");
30
66
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
31
67
  const streamMessageFormatter_1 = require("../utils/streamMessageFormatter");
@@ -36,7 +72,10 @@ const modeUi_1 = require("../ui/modeUi");
36
72
  const modelsUi_1 = require("../ui/modelsUi");
37
73
  const templateUi_1 = require("../ui/templateUi");
38
74
  const autoAcceptUi_1 = require("../ui/autoAcceptUi");
75
+ const outputUi_1 = require("../ui/outputUi");
39
76
  const screenshotUi_1 = require("../ui/screenshotUi");
77
+ const userPreferenceRepository_1 = require("../database/userPreferenceRepository");
78
+ const plainTextFormatter_1 = require("../utils/plainTextFormatter");
40
79
  const interactionCreateHandler_1 = require("../events/interactionCreateHandler");
41
80
  const messageCreateHandler_1 = require("../events/messageCreateHandler");
42
81
  // =============================================================================
@@ -60,6 +99,8 @@ const PHASE_ICONS = {
60
99
  };
61
100
  const MAX_OUTBOUND_GENERATED_IMAGES = 4;
62
101
  const RESPONSE_DELIVERY_MODE = (0, config_1.resolveResponseDeliveryMode)();
102
+ /** Tracks channel IDs where /stop was explicitly invoked by the user */
103
+ const userStopRequestedChannels = new Set();
63
104
  const getResponseDeliveryModeForTest = () => RESPONSE_DELIVERY_MODE;
64
105
  exports.getResponseDeliveryModeForTest = getResponseDeliveryModeForTest;
65
106
  function createSerialTaskQueueForTest(queueName, traceId) {
@@ -92,6 +133,17 @@ function createSerialTaskQueueForTest(queueName, traceId) {
92
133
  * - Visualize the flow of planning/analysis/execution confirmation/implementation as logs
93
134
  */
94
135
  async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService, modelService, inboundImages = [], options) {
136
+ // Completion signal — called exactly once when the entire prompt lifecycle ends
137
+ let completionSignaled = false;
138
+ const signalCompletion = (exitPath) => {
139
+ if (completionSignaled)
140
+ return;
141
+ completionSignaled = true;
142
+ logger_1.logger.debug(`[sendPrompt:${message.channelId}] signalCompletion via ${exitPath}`);
143
+ options?.onFullCompletion?.();
144
+ };
145
+ // Resolve output format once at the start (no mid-response switches)
146
+ const outputFormat = options?.userPrefRepo?.getOutputFormat(message.author.id) ?? 'embed';
95
147
  // Add reaction to acknowledge command receipt
96
148
  await message.react('👀').catch(() => { });
97
149
  const channel = (message.channel && 'send' in message.channel) ? message.channel : null;
@@ -102,6 +154,13 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
102
154
  const sendEmbed = (title, description, color, fields, footerText) => enqueueGeneral(async () => {
103
155
  if (!channel)
104
156
  return;
157
+ if (outputFormat === 'plain') {
158
+ const chunks = (0, plainTextFormatter_1.formatAsPlainText)({ title, description, fields, footerText });
159
+ for (const chunk of chunks) {
160
+ await channel.send({ content: chunk }).catch(() => { });
161
+ }
162
+ return;
163
+ }
105
164
  const embed = new discord_js_1.EmbedBuilder()
106
165
  .setTitle(title)
107
166
  .setDescription(description)
@@ -221,9 +280,10 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
221
280
  }
222
281
  };
223
282
  if (!cdp.isConnected()) {
224
- await sendEmbed(`${PHASE_ICONS.error} Connection Error`, 'Not connected to Antigravity.\nStart with `open -a Antigravity --args --remote-debugging-port=9223`, then send a message to auto-connect.', PHASE_COLORS.error);
283
+ await sendEmbed(`${PHASE_ICONS.error} Connection Error`, `Not connected to Antigravity.\nStart with \`${(0, pathUtils_1.getAntigravityCdpHint)(9223)}\`, then send a message to auto-connect.`, PHASE_COLORS.error);
225
284
  await clearWatchingReaction();
226
285
  await message.react('❌').catch(() => { });
286
+ signalCompletion('cdp-disconnected');
227
287
  return;
228
288
  }
229
289
  const localMode = modeService.getCurrentMode();
@@ -276,6 +336,30 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
276
336
  return;
277
337
  if (!channel)
278
338
  return;
339
+ if (outputFormat === 'plain') {
340
+ const formatted = (0, discordFormatter_1.formatForDiscord)((rawText || '').trim());
341
+ const plainChunks = (0, plainTextFormatter_1.splitPlainText)(`**${title}**\n${formatted}\n_${footerText}_`);
342
+ const renderKey = `${title}|plain|${footerText}|${plainChunks.join('\n<<<PAGE_BREAK>>>\n')}`;
343
+ if (renderKey === lastLiveResponseKey && liveResponseMessages.length > 0)
344
+ return;
345
+ lastLiveResponseKey = renderKey;
346
+ for (let i = 0; i < plainChunks.length; i++) {
347
+ if (!liveResponseMessages[i]) {
348
+ liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
349
+ continue;
350
+ }
351
+ await liveResponseMessages[i].edit({ content: plainChunks[i] }).catch(async () => {
352
+ liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
353
+ });
354
+ }
355
+ while (liveResponseMessages.length > plainChunks.length) {
356
+ const extra = liveResponseMessages.pop();
357
+ if (!extra)
358
+ continue;
359
+ await extra.delete().catch(() => { });
360
+ }
361
+ return;
362
+ }
279
363
  const descriptions = buildLiveResponseDescriptions(rawText);
280
364
  const renderKey = `${title}|${color}|${footerText}|${descriptions.join('\n<<<PAGE_BREAK>>>\n')}`;
281
365
  if (renderKey === lastLiveResponseKey && liveResponseMessages.length > 0) {
@@ -312,6 +396,31 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
312
396
  return;
313
397
  if (!channel)
314
398
  return;
399
+ if (outputFormat === 'plain') {
400
+ const formatted = (0, discordFormatter_1.formatForDiscord)((rawText || '').trim());
401
+ const plainContent = `**${title}**\n${formatted}\n_${footerText}_`;
402
+ const plainChunks = (0, plainTextFormatter_1.splitPlainText)(plainContent);
403
+ const renderKey = `${title}|plain|${footerText}|${plainChunks.join('\n<<<PAGE_BREAK>>>\n')}`;
404
+ if (renderKey === lastLiveActivityKey && liveActivityMessages.length > 0)
405
+ return;
406
+ lastLiveActivityKey = renderKey;
407
+ for (let i = 0; i < plainChunks.length; i++) {
408
+ if (!liveActivityMessages[i]) {
409
+ liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
410
+ continue;
411
+ }
412
+ await liveActivityMessages[i].edit({ content: plainChunks[i] }).catch(async () => {
413
+ liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
414
+ });
415
+ }
416
+ while (liveActivityMessages.length > plainChunks.length) {
417
+ const extra = liveActivityMessages.pop();
418
+ if (!extra)
419
+ continue;
420
+ await extra.delete().catch(() => { });
421
+ }
422
+ return;
423
+ }
315
424
  const descriptions = buildLiveActivityDescriptions(rawText);
316
425
  const renderKey = `${title}|${color}|${footerText}|${descriptions.join('\n<<<PAGE_BREAK>>>\n')}`;
317
426
  if (renderKey === lastLiveActivityKey && liveActivityMessages.length > 0) {
@@ -357,6 +466,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
357
466
  await sendEmbed(`${PHASE_ICONS.error} Message Injection Failed`, `Failed to send message: ${injectResult.error}`, PHASE_COLORS.error);
358
467
  await clearWatchingReaction();
359
468
  await message.react('❌').catch(() => { });
469
+ signalCompletion('inject-failed');
360
470
  return;
361
471
  }
362
472
  const startTime = Date.now();
@@ -387,7 +497,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
387
497
  onProgress: (text) => {
388
498
  if (isFinalized)
389
499
  return;
390
- // TODO: Re-enable live output streaming after RESPONSE_TEXT reliably excludes process logs.
500
+ // Live output streaming disabled: RESPONSE_TEXT currently includes process logs (see #1).
391
501
  const separated = (0, discordFormatter_1.splitOutputAndLogs)(text);
392
502
  if (separated.output && separated.output.trim().length > 0) {
393
503
  lastProgressText = separated.output;
@@ -396,86 +506,126 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
396
506
  onComplete: async (finalText) => {
397
507
  isFinalized = true;
398
508
  try {
399
- const elapsed = Math.round((Date.now() - startTime) / 1000);
400
- const responseText = (finalText && finalText.trim().length > 0)
401
- ? finalText
402
- : lastProgressText;
403
- const emergencyText = (!responseText || responseText.trim().length === 0)
404
- ? await tryEmergencyExtractText()
405
- : '';
406
- const finalResponseText = responseText && responseText.trim().length > 0
407
- ? responseText
408
- : emergencyText;
409
- const separated = (0, discordFormatter_1.splitOutputAndLogs)(finalResponseText);
410
- const finalOutputText = separated.output || finalResponseText;
411
- // Process logs are now collected by onProcessLog callback directly;
412
- // sanitizeActivityLines is NOT applied because it would strip the very
413
- // content we want to display (activity messages, tool names, etc.)
414
- const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
415
- if (finalLogText && finalLogText.trim().length > 0) {
416
- logger_1.logger.divider('Process Log');
417
- console.info(finalLogText);
418
- }
419
- if (finalOutputText && finalOutputText.trim().length > 0) {
420
- logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
421
- console.info(finalOutputText);
422
- }
423
- logger_1.logger.divider();
424
- liveActivityUpdateVersion += 1;
425
- const activityVersion = liveActivityUpdateVersion;
426
- await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Process log`), {
427
- source: 'complete',
428
- expectedVersion: activityVersion,
429
- });
430
- liveResponseUpdateVersion += 1;
431
- const responseVersion = liveResponseUpdateVersion;
432
- if (finalOutputText && finalOutputText.trim().length > 0) {
433
- await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Complete`), {
434
- source: 'complete',
435
- expectedVersion: responseVersion,
436
- });
509
+ // If the user explicitly pressed /stop, skip output display entirely
510
+ const wasStoppedByUser = userStopRequestedChannels.delete(message.channelId);
511
+ if (wasStoppedByUser) {
512
+ logger_1.logger.info(`[sendPromptToAntigravity:${monitorTraceId}] Stopped by user — skipping output`);
513
+ await clearWatchingReaction();
514
+ await message.react('âšī¸').catch(() => { });
515
+ return;
437
516
  }
438
- else {
439
- await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Complete`, (0, i18n_1.t)('Failed to extract response. Use `/screenshot` to verify.'), PHASE_COLORS.complete, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Complete`), {
517
+ try {
518
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
519
+ const isQuotaError = monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected();
520
+ // Quota early exit — skip text extraction, output logging, and embed entirely
521
+ if (isQuotaError) {
522
+ const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
523
+ if (finalLogText && finalLogText.trim().length > 0) {
524
+ logger_1.logger.divider('Process Log');
525
+ console.info(finalLogText);
526
+ }
527
+ logger_1.logger.divider();
528
+ liveActivityUpdateVersion += 1;
529
+ await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Process log`), {
530
+ source: 'complete',
531
+ expectedVersion: liveActivityUpdateVersion,
532
+ });
533
+ liveResponseUpdateVersion += 1;
534
+ await upsertLiveResponseEmbeds('âš ī¸ Model Quota Reached', 'Model quota limit reached. Please wait or switch to a different model.', 0xFF6B6B, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Quota Reached`), {
535
+ source: 'complete',
536
+ expectedVersion: liveResponseUpdateVersion,
537
+ });
538
+ try {
539
+ const modelsPayload = await (0, modelsUi_1.buildModelsUI)(cdp, () => bridge.quota.fetchQuota());
540
+ if (modelsPayload && channel) {
541
+ await channel.send({ ...modelsPayload });
542
+ }
543
+ }
544
+ catch (e) {
545
+ logger_1.logger.error('[Quota] Failed to send model selection UI:', e);
546
+ }
547
+ await clearWatchingReaction();
548
+ await message.react('âš ī¸').catch(() => { });
549
+ return;
550
+ }
551
+ // Normal path — extract final text
552
+ const responseText = (finalText && finalText.trim().length > 0)
553
+ ? finalText
554
+ : lastProgressText;
555
+ const emergencyText = (!responseText || responseText.trim().length === 0)
556
+ ? await tryEmergencyExtractText()
557
+ : '';
558
+ const finalResponseText = responseText && responseText.trim().length > 0
559
+ ? responseText
560
+ : emergencyText;
561
+ const separated = (0, discordFormatter_1.splitOutputAndLogs)(finalResponseText);
562
+ const finalOutputText = separated.output || finalResponseText;
563
+ // Process logs are now collected by onProcessLog callback directly;
564
+ // sanitizeActivityLines is NOT applied because it would strip the very
565
+ // content we want to display (activity messages, tool names, etc.)
566
+ const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
567
+ if (finalLogText && finalLogText.trim().length > 0) {
568
+ logger_1.logger.divider('Process Log');
569
+ console.info(finalLogText);
570
+ }
571
+ if (finalOutputText && finalOutputText.trim().length > 0) {
572
+ logger_1.logger.divider(`Output (${finalOutputText.length} chars)`);
573
+ console.info(finalOutputText);
574
+ }
575
+ logger_1.logger.divider();
576
+ liveActivityUpdateVersion += 1;
577
+ const activityVersion = liveActivityUpdateVersion;
578
+ await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Process log`), {
440
579
  source: 'complete',
441
- expectedVersion: responseVersion,
580
+ expectedVersion: activityVersion,
442
581
  });
443
- }
444
- if (options && message.guild) {
445
- try {
446
- const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
447
- if (sessionInfo && sessionInfo.hasActiveChat && sessionInfo.title && sessionInfo.title !== (0, i18n_1.t)('(Untitled)')) {
448
- const session = options.chatSessionRepo.findByChannelId(message.channelId);
449
- const workspaceDirName = session
450
- ? bridge.pool.extractDirName(session.workspacePath)
451
- : cdp.getCurrentWorkspaceName();
452
- if (workspaceDirName) {
453
- (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, workspaceDirName, sessionInfo.title, message.channel);
454
- }
455
- const newName = options.titleGenerator.sanitizeForChannelName(sessionInfo.title);
456
- if (session && session.displayName !== sessionInfo.title) {
457
- const formattedName = `${session.sessionNumber}-${newName}`;
458
- await options.channelManager.renameChannel(message.guild, message.channelId, formattedName);
459
- options.chatSessionRepo.updateDisplayName(message.channelId, sessionInfo.title);
582
+ liveResponseUpdateVersion += 1;
583
+ const responseVersion = liveResponseUpdateVersion;
584
+ if (finalOutputText && finalOutputText.trim().length > 0) {
585
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Complete`), {
586
+ source: 'complete',
587
+ expectedVersion: responseVersion,
588
+ });
589
+ }
590
+ else {
591
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Complete`, (0, i18n_1.t)('Failed to extract response. Use `/screenshot` to verify.'), PHASE_COLORS.complete, (0, i18n_1.t)(`âąī¸ Time: ${elapsed}s | Complete`), {
592
+ source: 'complete',
593
+ expectedVersion: responseVersion,
594
+ });
595
+ }
596
+ if (options && message.guild) {
597
+ try {
598
+ const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
599
+ if (sessionInfo && sessionInfo.hasActiveChat && sessionInfo.title && sessionInfo.title !== (0, i18n_1.t)('(Untitled)')) {
600
+ const session = options.chatSessionRepo.findByChannelId(message.channelId);
601
+ const projectName = session
602
+ ? bridge.pool.extractProjectName(session.workspacePath)
603
+ : cdp.getCurrentWorkspaceName();
604
+ if (projectName) {
605
+ (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, sessionInfo.title, message.channel);
606
+ }
607
+ const newName = options.titleGenerator.sanitizeForChannelName(sessionInfo.title);
608
+ if (session && session.displayName !== sessionInfo.title) {
609
+ const formattedName = `${session.sessionNumber}-${newName}`;
610
+ await options.channelManager.renameChannel(message.guild, message.channelId, formattedName);
611
+ options.chatSessionRepo.updateDisplayName(message.channelId, sessionInfo.title);
612
+ }
460
613
  }
461
614
  }
615
+ catch (e) {
616
+ logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
617
+ }
462
618
  }
463
- catch (e) {
464
- logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
465
- }
466
- }
467
- if (monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected()) {
468
- await sendEmbed('âš ī¸ Model Quota Reached', 'Model quota limit reached. Please wait or switch to a different model with `/model`.', 0xFF6B6B, undefined, 'Quota Reached — consider switching models');
619
+ await sendGeneratedImages(finalOutputText || '');
469
620
  await clearWatchingReaction();
470
- await message.react('âš ī¸').catch(() => { });
471
- return;
621
+ await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : 'âš ī¸').catch(() => { });
622
+ }
623
+ catch (error) {
624
+ logger_1.logger.error(`[sendPromptToAntigravity:${monitorTraceId}] onComplete failed:`, error);
472
625
  }
473
- await sendGeneratedImages(finalOutputText || '');
474
- await clearWatchingReaction();
475
- await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : 'âš ī¸').catch(() => { });
476
626
  }
477
- catch (error) {
478
- logger_1.logger.error(`[sendPromptToAntigravity:${monitorTraceId}] onComplete failed:`, error);
627
+ finally {
628
+ signalCompletion('onComplete');
479
629
  }
480
630
  },
481
631
  onTimeout: async (lastText) => {
@@ -508,27 +658,48 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
508
658
  catch (error) {
509
659
  logger_1.logger.error(`[sendPromptToAntigravity:${monitorTraceId}] onTimeout failed:`, error);
510
660
  }
661
+ finally {
662
+ signalCompletion('onTimeout');
663
+ }
511
664
  },
512
665
  });
513
666
  await monitor.start();
667
+ // 1-second elapsed timer — updates footer independently of process log events
668
+ const elapsedTimer = setInterval(() => {
669
+ if (isFinalized) {
670
+ clearInterval(elapsedTimer);
671
+ return;
672
+ }
673
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
674
+ liveActivityUpdateVersion += 1;
675
+ const activityVersion = liveActivityUpdateVersion;
676
+ upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, lastActivityLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`âąī¸ Elapsed: ${elapsed}s | Process log`), {
677
+ source: 'elapsed-tick',
678
+ expectedVersion: activityVersion,
679
+ skipWhenFinalized: true,
680
+ }).catch(() => { });
681
+ }, 1000);
514
682
  }
515
683
  catch (e) {
516
684
  isFinalized = true;
517
685
  await sendEmbed(`${PHASE_ICONS.error} Error`, (0, i18n_1.t)(`Error occurred during processing: ${e.message}`), PHASE_COLORS.error);
518
686
  await clearWatchingReaction();
519
687
  await message.react('❌').catch(() => { });
688
+ signalCompletion('top-level-catch');
520
689
  }
521
690
  }
522
691
  // =============================================================================
523
692
  // Bot main entry point
524
693
  // =============================================================================
525
- const startBot = async () => {
694
+ const startBot = async (cliLogLevel) => {
526
695
  const config = (0, config_1.loadConfig)();
696
+ logger_1.logger.setLogLevel(cliLogLevel ?? config.logLevel);
527
697
  const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : 'antigravity.db';
528
698
  const db = new better_sqlite3_1.default(dbPath);
529
699
  const modeService = new modeService_1.ModeService();
530
700
  const modelService = new modelService_1.ModelService();
531
701
  const templateRepo = new templateRepository_1.TemplateRepository(db);
702
+ const userPrefRepo = new userPreferenceRepository_1.UserPreferenceRepository(db);
532
703
  const workspaceBindingRepo = new workspaceBindingRepository_1.WorkspaceBindingRepository(db);
533
704
  const chatSessionRepo = new chatSessionRepository_1.ChatSessionRepository(db);
534
705
  const workspaceService = new workspaceService_1.WorkspaceService(config.workspaceBaseDir);
@@ -546,7 +717,7 @@ const startBot = async () => {
546
717
  modelService,
547
718
  sendPromptImpl: sendPromptToAntigravity,
548
719
  });
549
- // Initialize command handlers
720
+ // Initialize command handlers (joinHandler is created after client, see below)
550
721
  const wsHandler = new workspaceCommandHandler_1.WorkspaceCommandHandler(workspaceBindingRepo, chatSessionRepo, workspaceService, channelManager);
551
722
  const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool);
552
723
  const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
@@ -558,14 +729,45 @@ const startBot = async () => {
558
729
  discord_js_1.GatewayIntentBits.MessageContent,
559
730
  ]
560
731
  });
732
+ const joinHandler = new joinCommandHandler_1.JoinCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, bridge.pool, workspaceService, client);
561
733
  client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
562
- logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag}`);
734
+ logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
563
735
  try {
564
736
  await (0, registerSlashCommands_1.registerSlashCommands)(config.discordToken, config.clientId, config.guildId);
565
737
  }
566
738
  catch (error) {
567
739
  logger_1.logger.warn('Failed to register slash commands, but text commands remain available.');
568
740
  }
741
+ // Startup dashboard embed
742
+ try {
743
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
744
+ const pkg = await Promise.resolve().then(() => __importStar(require('../../package.json')));
745
+ const version = pkg.default?.version ?? pkg.version ?? 'unknown';
746
+ const projects = workspaceService.scanWorkspaces();
747
+ // Check CDP connection status
748
+ const activeWorkspaces = bridge.pool.getActiveWorkspaceNames();
749
+ const cdpStatus = activeWorkspaces.length > 0
750
+ ? `Connected (${activeWorkspaces.join(', ')})`
751
+ : 'Not connected';
752
+ const dashboardEmbed = new discord_js_1.EmbedBuilder()
753
+ .setTitle('LazyGravity Online')
754
+ .setColor(0x57F287)
755
+ .addFields({ name: 'Version', value: version, inline: true }, { name: 'Node.js', value: process.versions.node, inline: true }, { name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true }, { name: 'CDP', value: cdpStatus, inline: true }, { name: 'Model', value: modelService.getCurrentModel(), inline: true }, { name: 'Mode', value: modeService.getCurrentMode(), inline: true }, { name: 'Projects', value: `${projects.length} registered`, inline: true }, { name: 'Extraction', value: config.extractionMode, inline: true })
756
+ .setFooter({ text: `Started at ${new Date().toLocaleString()}` })
757
+ .setTimestamp();
758
+ // Send to the first available text channel in the guild
759
+ const guild = readyClient.guilds.cache.first();
760
+ if (guild) {
761
+ const channel = guild.channels.cache.find((ch) => ch.isTextBased() && !ch.isVoiceBased() && ch.permissionsFor(readyClient.user)?.has('SendMessages'));
762
+ if (channel && channel.isTextBased()) {
763
+ await channel.send({ embeds: [dashboardEmbed] });
764
+ logger_1.logger.info('Startup dashboard embed sent.');
765
+ }
766
+ }
767
+ }
768
+ catch (error) {
769
+ logger_1.logger.warn('Failed to send startup dashboard embed:', error);
770
+ }
569
771
  });
570
772
  // [Discord Interactions API] Slash command interaction handler
571
773
  client.on(discord_js_1.Events.InteractionCreate, (0, interactionCreateHandler_1.createInteractionCreateHandler)({
@@ -583,7 +785,11 @@ const startBot = async () => {
583
785
  sendAutoAcceptUI: autoAcceptUi_1.sendAutoAcceptUI,
584
786
  getCurrentCdp: cdpBridgeManager_1.getCurrentCdp,
585
787
  parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
586
- handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo),
788
+ parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
789
+ parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
790
+ joinHandler,
791
+ userPrefRepo,
792
+ handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo),
587
793
  handleTemplateUse: async (interaction, templateId) => {
588
794
  const template = templateRepo.findById(templateId);
589
795
  if (!template) {
@@ -600,15 +806,17 @@ const startBot = async () => {
600
806
  if (workspacePath) {
601
807
  try {
602
808
  cdp = await bridge.pool.getOrConnect(workspacePath);
603
- const dirName = bridge.pool.extractDirName(workspacePath);
604
- bridge.lastActiveWorkspace = dirName;
809
+ const projectName = bridge.pool.extractProjectName(workspacePath);
810
+ bridge.lastActiveWorkspace = projectName;
605
811
  bridge.lastActiveChannel = interaction.channel;
606
- (0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(bridge, dirName, interaction.channel);
812
+ (0, cdpBridgeManager_1.registerApprovalWorkspaceChannel)(bridge, projectName, interaction.channel);
607
813
  const session = chatSessionRepo.findByChannelId(channelId);
608
814
  if (session?.displayName) {
609
- (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, dirName, session.displayName, interaction.channel);
815
+ (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, session.displayName, interaction.channel);
610
816
  }
611
- (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, dirName, client);
817
+ (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName, client);
818
+ (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName, client);
819
+ (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName, client);
612
820
  }
613
821
  catch (e) {
614
822
  await interaction.followUp({
@@ -642,6 +850,7 @@ const startBot = async () => {
642
850
  chatSessionRepo,
643
851
  channelManager,
644
852
  titleGenerator,
853
+ userPrefRepo,
645
854
  },
646
855
  });
647
856
  }
@@ -669,6 +878,7 @@ const startBot = async () => {
669
878
  }),
670
879
  autoRenameChannel,
671
880
  handleScreenshot: screenshotUi_1.handleScreenshot,
881
+ userPrefRepo,
672
882
  }));
673
883
  await client.login(config.discordToken);
674
884
  };
@@ -696,55 +906,82 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
696
906
  /**
697
907
  * Handle Discord Interactions API slash commands
698
908
  */
699
- async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo) {
909
+ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo) {
700
910
  const commandName = interaction.commandName;
701
911
  switch (commandName) {
702
912
  case 'help': {
913
+ const helpFields = [
914
+ {
915
+ name: 'đŸ’Ŧ Chat', value: [
916
+ '`/new` — Start a new chat session',
917
+ '`/chat` — Show current session info + list',
918
+ ].join('\n')
919
+ },
920
+ {
921
+ name: '🔗 Session', value: [
922
+ '`/join` — Join an existing Antigravity session',
923
+ '`/mirror` — Toggle PC→Discord mirroring ON/OFF',
924
+ ].join('\n')
925
+ },
926
+ {
927
+ name: 'âšī¸ Control', value: [
928
+ '`/stop` — Interrupt active LLM generation',
929
+ '`/screenshot` — Capture Antigravity screen',
930
+ ].join('\n')
931
+ },
932
+ {
933
+ name: 'âš™ī¸ Settings', value: [
934
+ '`/mode` — Display and change execution mode',
935
+ '`/model [name]` — Display and change LLM model',
936
+ '`/output [format]` — Toggle Embed / Plain Text output',
937
+ ].join('\n')
938
+ },
939
+ {
940
+ name: '📁 Projects', value: [
941
+ '`/project` — Display project list',
942
+ '`/project create <name>` — Create a new project',
943
+ ].join('\n')
944
+ },
945
+ {
946
+ name: '📝 Templates', value: [
947
+ '`/template list` — Show templates with execute buttons (click to run)',
948
+ '`/template add <name> <prompt>` — Register a template',
949
+ '`/template delete <name>` — Delete a template',
950
+ ].join('\n')
951
+ },
952
+ {
953
+ name: '🔧 System', value: [
954
+ '`/status` — Display overall bot status',
955
+ '`/autoaccept` — Toggle auto-approve mode for approval dialogs via buttons',
956
+ '`/logs [lines] [level]` — View recent bot logs',
957
+ '`/cleanup [days]` — Clean up unused channels/categories',
958
+ '`/help` — Show this help',
959
+ ].join('\n')
960
+ },
961
+ ];
962
+ const helpOutputFormat = userPrefRepo?.getOutputFormat(interaction.user.id) ?? 'embed';
963
+ if (helpOutputFormat === 'plain') {
964
+ const chunks = (0, plainTextFormatter_1.formatAsPlainText)({
965
+ title: '📖 LazyGravity Commands',
966
+ description: 'Commands for controlling Antigravity from Discord.',
967
+ fields: helpFields,
968
+ footerText: 'Text messages are sent directly to Antigravity',
969
+ });
970
+ await interaction.editReply({ content: chunks[0] });
971
+ break;
972
+ }
703
973
  const embed = new discord_js_1.EmbedBuilder()
704
974
  .setTitle('📖 LazyGravity Commands')
705
975
  .setColor(0x5865F2)
706
976
  .setDescription('Commands for controlling Antigravity from Discord.')
707
- .addFields({
708
- name: 'đŸ’Ŧ Chat', value: [
709
- '`/new` — Start a new chat session',
710
- '`/chat` — Show current session info + list',
711
- ].join('\n')
712
- }, {
713
- name: 'âšī¸ Control', value: [
714
- '`/stop` — Interrupt active LLM generation',
715
- '`/screenshot` — Capture Antigravity screen',
716
- ].join('\n')
717
- }, {
718
- name: 'âš™ī¸ Settings', value: [
719
- '`/mode` — Display and change execution mode',
720
- '`/model [name]` — Display and change LLM model',
721
- ].join('\n')
722
- }, {
723
- name: '📁 Projects', value: [
724
- '`/project` — Display project list',
725
- '`/project create <name>` — Create a new project',
726
- ].join('\n')
727
- }, {
728
- name: '📝 Templates', value: [
729
- '`/template list` — Show templates with execute buttons (click to run)',
730
- '`/template add <name> <prompt>` — Register a template',
731
- '`/template delete <name>` — Delete a template',
732
- ].join('\n')
733
- }, {
734
- name: '🔧 System', value: [
735
- '`/status` — Display overall bot status',
736
- '`/autoaccept` — Toggle auto-approve mode for approval dialogs via buttons',
737
- '`/cleanup [days]` — Clean up unused channels/categories',
738
- '`/help` — Show this help',
739
- ].join('\n')
740
- })
977
+ .addFields(...helpFields)
741
978
  .setFooter({ text: 'Text messages are sent directly to Antigravity' })
742
979
  .setTimestamp();
743
980
  await interaction.editReply({ embeds: [embed] });
744
981
  break;
745
982
  }
746
983
  case 'mode': {
747
- await (0, modeUi_1.sendModeUI)(interaction, modeService);
984
+ await (0, modeUi_1.sendModeUI)(interaction, modeService, { getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge) });
748
985
  break;
749
986
  }
750
987
  case 'model': {
@@ -805,23 +1042,46 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
805
1042
  return cdp ? 'CDP Connected' : 'Disconnected';
806
1043
  })();
807
1044
  const currentMode = modeService.getCurrentMode();
808
- const embed = new discord_js_1.EmbedBuilder()
809
- .setTitle('🔧 Bot Status')
810
- .setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
811
- .addFields({ name: 'CDP Connection', value: activeNames.length > 0 ? `đŸŸĸ ${activeNames.length} project(s) connected` : 'âšĒ Disconnected', inline: true }, { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true }, { name: 'Auto Approve', value: autoAcceptService.isEnabled() ? 'đŸŸĸ ON' : 'âšĒ OFF', inline: true })
812
- .setTimestamp();
1045
+ const mirroringWorkspaces = activeNames.filter((name) => bridge.pool.getUserMessageDetector(name)?.isActive());
1046
+ const mirrorStatus = mirroringWorkspaces.length > 0
1047
+ ? `📡 ON (${mirroringWorkspaces.join(', ')})`
1048
+ : 'âšĒ OFF';
1049
+ const statusFields = [
1050
+ { name: 'CDP Connection', value: activeNames.length > 0 ? `đŸŸĸ ${activeNames.length} project(s) connected` : 'âšĒ Disconnected', inline: true },
1051
+ { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true },
1052
+ { name: 'Auto Approve', value: autoAcceptService.isEnabled() ? 'đŸŸĸ ON' : 'âšĒ OFF', inline: true },
1053
+ { name: 'Mirroring', value: mirrorStatus, inline: true },
1054
+ ];
1055
+ let statusDescription = '';
813
1056
  if (activeNames.length > 0) {
814
1057
  const lines = activeNames.map((name) => {
815
1058
  const cdp = bridge.pool.getConnected(name);
816
1059
  const contexts = cdp ? cdp.getContexts().length : 0;
817
1060
  const detectorActive = bridge.pool.getApprovalDetector(name)?.isActive() ? ' [Detecting]' : '';
818
- return `â€ĸ **${name}** — Contexts: ${contexts}${detectorActive}`;
1061
+ const mirrorActive = bridge.pool.getUserMessageDetector(name)?.isActive() ? ' [Mirror]' : '';
1062
+ return `â€ĸ **${name}** — Contexts: ${contexts}${detectorActive}${mirrorActive}`;
819
1063
  });
820
- embed.setDescription(`**Connected Projects:**\n${lines.join('\n')}`);
1064
+ statusDescription = `**Connected Projects:**\n${lines.join('\n')}`;
821
1065
  }
822
1066
  else {
823
- embed.setDescription('Send a message to auto-connect to a project.');
1067
+ statusDescription = 'Send a message to auto-connect to a project.';
1068
+ }
1069
+ const statusOutputFormat = userPrefRepo?.getOutputFormat(interaction.user.id) ?? 'embed';
1070
+ if (statusOutputFormat === 'plain') {
1071
+ const chunks = (0, plainTextFormatter_1.formatAsPlainText)({
1072
+ title: '🔧 Bot Status',
1073
+ description: statusDescription,
1074
+ fields: statusFields,
1075
+ });
1076
+ await interaction.editReply({ content: chunks[0] });
1077
+ break;
824
1078
  }
1079
+ const embed = new discord_js_1.EmbedBuilder()
1080
+ .setTitle('🔧 Bot Status')
1081
+ .setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
1082
+ .addFields(...statusFields)
1083
+ .setDescription(statusDescription)
1084
+ .setTimestamp();
825
1085
  await interaction.editReply({ embeds: [embed] });
826
1086
  break;
827
1087
  }
@@ -835,6 +1095,23 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
835
1095
  await interaction.editReply({ content: result.message });
836
1096
  break;
837
1097
  }
1098
+ case 'output': {
1099
+ if (!userPrefRepo) {
1100
+ await interaction.editReply({ content: 'Output preference service not available.' });
1101
+ break;
1102
+ }
1103
+ const requestedFormat = interaction.options.getString('format');
1104
+ if (!requestedFormat) {
1105
+ const currentFormat = userPrefRepo.getOutputFormat(interaction.user.id);
1106
+ await (0, outputUi_1.sendOutputUI)(interaction, currentFormat);
1107
+ break;
1108
+ }
1109
+ const format = requestedFormat === 'plain' ? 'plain' : 'embed';
1110
+ userPrefRepo.setOutputFormat(interaction.user.id, format);
1111
+ const label = format === 'plain' ? 'Plain Text' : 'Embed';
1112
+ await interaction.editReply({ content: `Output format changed to **${label}**.` });
1113
+ break;
1114
+ }
838
1115
  case 'screenshot': {
839
1116
  await (0, screenshotUi_1.handleScreenshot)(interaction, (0, cdpBridgeManager_1.getCurrentCdp)(bridge));
840
1117
  break;
@@ -858,6 +1135,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
858
1135
  const result = await cdp.call('Runtime.evaluate', callParams);
859
1136
  const value = result?.result?.value;
860
1137
  if (value?.ok) {
1138
+ userStopRequestedChannels.add(interaction.channelId);
861
1139
  const embed = new discord_js_1.EmbedBuilder()
862
1140
  .setTitle('âšī¸ Generation Interrupted')
863
1141
  .setDescription('AI response generation was safely stopped.')
@@ -902,10 +1180,51 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
902
1180
  await chatHandler.handleChat(interaction);
903
1181
  break;
904
1182
  }
1183
+ case 'join': {
1184
+ if (joinHandler) {
1185
+ await joinHandler.handleJoin(interaction, bridge);
1186
+ }
1187
+ else {
1188
+ await interaction.editReply({ content: (0, i18n_1.t)('âš ī¸ Join handler not available.') });
1189
+ }
1190
+ break;
1191
+ }
1192
+ case 'mirror': {
1193
+ if (joinHandler) {
1194
+ await joinHandler.handleMirror(interaction, bridge);
1195
+ }
1196
+ else {
1197
+ await interaction.editReply({ content: (0, i18n_1.t)('âš ī¸ Mirror handler not available.') });
1198
+ }
1199
+ break;
1200
+ }
905
1201
  case 'cleanup': {
906
1202
  await cleanupHandler.handleCleanup(interaction);
907
1203
  break;
908
1204
  }
1205
+ case 'ping': {
1206
+ const apiLatency = interaction.client.ws.ping;
1207
+ await interaction.editReply({ content: `🏓 Pong! API Latency is **${apiLatency}ms**.` });
1208
+ break;
1209
+ }
1210
+ case 'logs': {
1211
+ const lines = interaction.options.getInteger('lines') ?? 50;
1212
+ const level = interaction.options.getString('level');
1213
+ const entries = logBuffer_1.logBuffer.getRecent(lines, level ?? undefined);
1214
+ if (entries.length === 0) {
1215
+ await interaction.editReply({ content: 'No log entries found.' });
1216
+ break;
1217
+ }
1218
+ const formatted = entries
1219
+ .map((e) => `${e.timestamp.slice(11, 19)} ${e.message}`)
1220
+ .join('\n');
1221
+ const MAX_CONTENT = 1900;
1222
+ const codeBlock = formatted.length <= MAX_CONTENT
1223
+ ? `\`\`\`\n${formatted}\n\`\`\``
1224
+ : `\`\`\`\n${formatted.slice(0, MAX_CONTENT)}\n\`\`\`\n(truncated — showing ${MAX_CONTENT} chars of ${formatted.length})`;
1225
+ await interaction.editReply({ content: codeBlock });
1226
+ break;
1227
+ }
909
1228
  default:
910
1229
  await interaction.editReply({
911
1230
  content: `Unknown command: /${commandName}`,