lazy-gravity 0.6.1 → 0.7.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.
package/dist/bot/index.js CHANGED
@@ -38,6 +38,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.startBot = exports.getResponseDeliveryModeForTest = void 0;
40
40
  exports.createSerialTaskQueueForTest = createSerialTaskQueueForTest;
41
+ exports.handleSlashInteraction = handleSlashInteraction;
42
+ const sessionPickerUi_1 = require("../ui/sessionPickerUi");
43
+ const telegramJoinCommand_1 = require("./telegramJoinCommand");
41
44
  const i18n_1 = require("../utils/i18n");
42
45
  const logger_1 = require("../utils/logger");
43
46
  const logBuffer_1 = require("../utils/logBuffer");
@@ -51,7 +54,9 @@ const modeService_1 = require("../services/modeService");
51
54
  const modelService_1 = require("../services/modelService");
52
55
  const defaultModelApplicator_1 = require("../services/defaultModelApplicator");
53
56
  const templateRepository_1 = require("../database/templateRepository");
57
+ const accountPreferenceRepository_1 = require("../database/accountPreferenceRepository");
54
58
  const workspaceBindingRepository_1 = require("../database/workspaceBindingRepository");
59
+ const channelPreferenceRepository_1 = require("../database/channelPreferenceRepository");
55
60
  const chatSessionRepository_1 = require("../database/chatSessionRepository");
56
61
  const workspaceService_1 = require("../services/workspaceService");
57
62
  const workspaceCommandHandler_1 = require("../commands/workspaceCommandHandler");
@@ -60,6 +65,8 @@ const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
60
65
  const channelManager_1 = require("../services/channelManager");
61
66
  const titleGeneratorService_1 = require("../services/titleGeneratorService");
62
67
  const joinCommandHandler_1 = require("../commands/joinCommandHandler");
68
+ // CDP integration services
69
+ const cdpService_1 = require("../services/cdpService");
63
70
  const chatSessionService_1 = require("../services/chatSessionService");
64
71
  const responseMonitor_1 = require("../services/responseMonitor");
65
72
  const antigravityLauncher_1 = require("../services/antigravityLauncher");
@@ -74,9 +81,11 @@ const modeUi_1 = require("../ui/modeUi");
74
81
  const modelsUi_1 = require("../ui/modelsUi");
75
82
  const templateUi_1 = require("../ui/templateUi");
76
83
  const autoAcceptUi_1 = require("../ui/autoAcceptUi");
84
+ const accountUi_1 = require("../ui/accountUi");
77
85
  const outputUi_1 = require("../ui/outputUi");
78
86
  const screenshotUi_1 = require("../ui/screenshotUi");
79
87
  const userPreferenceRepository_1 = require("../database/userPreferenceRepository");
88
+ const accountUtils_1 = require("../utils/accountUtils");
80
89
  const plainTextFormatter_1 = require("../utils/plainTextFormatter");
81
90
  const interactionCreateHandler_1 = require("../events/interactionCreateHandler");
82
91
  const messageCreateHandler_1 = require("../events/messageCreateHandler");
@@ -97,6 +106,8 @@ const modelButtonAction_1 = require("../handlers/modelButtonAction");
97
106
  const autoAcceptButtonAction_1 = require("../handlers/autoAcceptButtonAction");
98
107
  const templateButtonAction_1 = require("../handlers/templateButtonAction");
99
108
  const modeSelectAction_1 = require("../handlers/modeSelectAction");
109
+ const accountSelectAction_1 = require("../handlers/accountSelectAction");
110
+ const telegramStartupTarget_1 = require("./telegramStartupTarget");
100
111
  const channelManager_2 = require("../services/channelManager");
101
112
  /**
102
113
  * Normalize a candidate startup channel name for preference checks.
@@ -184,13 +195,19 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
184
195
  const enqueueGeneral = createSerialTaskQueueForTest('general', monitorTraceId);
185
196
  const enqueueResponse = createSerialTaskQueueForTest('response', monitorTraceId);
186
197
  const enqueueActivity = createSerialTaskQueueForTest('activity', monitorTraceId);
198
+ const logDeliveryError = (scope, error) => {
199
+ const messageText = error instanceof Error ? error.message : String(error);
200
+ logger_1.logger.warn(`[DiscordDelivery:${monitorTraceId}] ${scope} failed: ${messageText}`);
201
+ };
187
202
  const sendEmbed = (title, description, color, fields, footerText) => enqueueGeneral(async () => {
188
203
  if (!channel)
189
204
  return;
190
205
  if (outputFormat === 'plain') {
191
206
  const chunks = (0, plainTextFormatter_1.formatAsPlainText)({ title, description, fields, footerText });
192
207
  for (const chunk of chunks) {
193
- await channel.send({ content: chunk }).catch(() => { });
208
+ await channel.send({ content: chunk }).catch((error) => {
209
+ logDeliveryError('sendEmbed/plain/send', error);
210
+ });
194
211
  }
195
212
  return;
196
213
  }
@@ -205,7 +222,9 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
205
222
  if (footerText) {
206
223
  embed.setFooter({ text: footerText });
207
224
  }
208
- await channel.send({ embeds: [embed] }).catch(() => { });
225
+ await channel.send({ embeds: [embed] }).catch((error) => {
226
+ logDeliveryError('sendEmbed/embed/send', error);
227
+ });
209
228
  }, 'send-embed');
210
229
  const shouldTryGeneratedImages = (inputPrompt, responseText) => {
211
230
  const prompt = (inputPrompt || '').toLowerCase();
@@ -238,7 +257,9 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
238
257
  await channel.send({
239
258
  content: (0, i18n_1.t)(`🖼️ Detected generated images (${files.length})`),
240
259
  files,
241
- }).catch(() => { });
260
+ }).catch((error) => {
261
+ logDeliveryError('sendGeneratedImages/send', error);
262
+ });
242
263
  }, 'send-generated-images');
243
264
  };
244
265
  const tryEmergencyExtractText = async () => {
@@ -322,7 +343,9 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
322
343
  // Apply default model preference on CDP connect
323
344
  const defaultModelResult = await (0, defaultModelApplicator_1.applyDefaultModel)(cdp, modelService);
324
345
  if (defaultModelResult.stale && defaultModelResult.staleMessage && channel) {
325
- await channel.send(defaultModelResult.staleMessage).catch(() => { });
346
+ await channel.send(defaultModelResult.staleMessage).catch((error) => {
347
+ logDeliveryError('defaultModelResult/send', error);
348
+ });
326
349
  }
327
350
  const localMode = modeService.getCurrentMode();
328
351
  const modeName = modeService_1.MODE_UI_NAMES[localMode] || localMode;
@@ -383,11 +406,18 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
383
406
  lastLiveResponseKey = renderKey;
384
407
  for (let i = 0; i < plainChunks.length; i++) {
385
408
  if (!liveResponseMessages[i]) {
386
- liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
409
+ liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch((error) => {
410
+ logDeliveryError('liveResponse/plain/send', error);
411
+ return null;
412
+ });
387
413
  continue;
388
414
  }
389
- await liveResponseMessages[i].edit({ content: plainChunks[i] }).catch(async () => {
390
- liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
415
+ await liveResponseMessages[i].edit({ content: plainChunks[i] }).catch(async (error) => {
416
+ logDeliveryError('liveResponse/plain/edit', error);
417
+ liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch((sendError) => {
418
+ logDeliveryError('liveResponse/plain/resend', sendError);
419
+ return null;
420
+ });
391
421
  });
392
422
  }
393
423
  while (liveResponseMessages.length > plainChunks.length) {
@@ -412,11 +442,18 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
412
442
  .setFooter({ text: footerText })
413
443
  .setTimestamp();
414
444
  if (!liveResponseMessages[i]) {
415
- liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
445
+ liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch((error) => {
446
+ logDeliveryError('liveResponse/embed/send', error);
447
+ return null;
448
+ });
416
449
  continue;
417
450
  }
418
- await liveResponseMessages[i].edit({ embeds: [embed] }).catch(async () => {
419
- liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
451
+ await liveResponseMessages[i].edit({ embeds: [embed] }).catch(async (error) => {
452
+ logDeliveryError('liveResponse/embed/edit', error);
453
+ liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch((sendError) => {
454
+ logDeliveryError('liveResponse/embed/resend', sendError);
455
+ return null;
456
+ });
420
457
  });
421
458
  }
422
459
  // Delete excess messages if page count decreased
@@ -444,11 +481,18 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
444
481
  lastLiveActivityKey = renderKey;
445
482
  for (let i = 0; i < plainChunks.length; i++) {
446
483
  if (!liveActivityMessages[i]) {
447
- liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
484
+ liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch((error) => {
485
+ logDeliveryError('liveActivity/plain/send', error);
486
+ return null;
487
+ });
448
488
  continue;
449
489
  }
450
- await liveActivityMessages[i].edit({ content: plainChunks[i] }).catch(async () => {
451
- liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch(() => null);
490
+ await liveActivityMessages[i].edit({ content: plainChunks[i] }).catch(async (error) => {
491
+ logDeliveryError('liveActivity/plain/edit', error);
492
+ liveActivityMessages[i] = await channel.send({ content: plainChunks[i] }).catch((sendError) => {
493
+ logDeliveryError('liveActivity/plain/resend', sendError);
494
+ return null;
495
+ });
452
496
  });
453
497
  }
454
498
  while (liveActivityMessages.length > plainChunks.length) {
@@ -473,11 +517,18 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
473
517
  .setFooter({ text: footerText })
474
518
  .setTimestamp();
475
519
  if (!liveActivityMessages[i]) {
476
- liveActivityMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
520
+ liveActivityMessages[i] = await channel.send({ embeds: [embed] }).catch((error) => {
521
+ logDeliveryError('liveActivity/embed/send', error);
522
+ return null;
523
+ });
477
524
  continue;
478
525
  }
479
- await liveActivityMessages[i].edit({ embeds: [embed] }).catch(async () => {
480
- liveActivityMessages[i] = await channel.send({ embeds: [embed] }).catch(() => null);
526
+ await liveActivityMessages[i].edit({ embeds: [embed] }).catch(async (error) => {
527
+ logDeliveryError('liveActivity/embed/edit', error);
528
+ liveActivityMessages[i] = await channel.send({ embeds: [embed] }).catch((sendError) => {
529
+ logDeliveryError('liveActivity/embed/resend', sendError);
530
+ return null;
531
+ });
481
532
  });
482
533
  }
483
534
  while (liveActivityMessages.length > descriptions.length) {
@@ -488,6 +539,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
488
539
  }
489
540
  }, `upsert-activity:${opts?.source ?? 'unknown'}`);
490
541
  try {
542
+ const baseline = await (0, responseMonitor_1.captureResponseMonitorBaseline)(cdp);
491
543
  logger_1.logger.prompt(prompt);
492
544
  let injectResult;
493
545
  if (inboundImages.length > 0) {
@@ -516,6 +568,8 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
516
568
  maxDurationMs: options?.responseTimeoutMs,
517
569
  stopGoneConfirmCount: 3,
518
570
  extractionMode: options?.extractionMode,
571
+ initialBaselineText: baseline.text,
572
+ initialSeenProcessLogKeys: baseline.processLogKeys,
519
573
  onPhaseChange: (_phase, _text) => {
520
574
  // Phase transitions are already logged inside ResponseMonitor.setPhase()
521
575
  },
@@ -578,7 +632,9 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
578
632
  try {
579
633
  const modelsPayload = await (0, modelsUi_1.buildModelsUI)(cdp, () => bridge.quota.fetchQuota());
580
634
  if (modelsPayload && channel) {
581
- await channel.send({ ...modelsPayload });
635
+ await channel.send({ ...modelsPayload }).catch((error) => {
636
+ logDeliveryError('quota/modelsPayload/send', error);
637
+ });
582
638
  }
583
639
  }
584
640
  catch (e) {
@@ -741,6 +797,8 @@ const startBot = async (cliLogLevel) => {
741
797
  const modelService = new modelService_1.ModelService();
742
798
  const templateRepo = new templateRepository_1.TemplateRepository(db);
743
799
  const userPrefRepo = new userPreferenceRepository_1.UserPreferenceRepository(db);
800
+ const accountPrefRepo = new accountPreferenceRepository_1.AccountPreferenceRepository(db);
801
+ const channelPrefRepo = new channelPreferenceRepository_1.ChannelPreferenceRepository(db);
744
802
  // Eagerly load default model from DB (single-user bot optimization)
745
803
  try {
746
804
  const firstUser = db.prepare('SELECT user_id FROM user_preferences LIMIT 1').get();
@@ -759,7 +817,11 @@ const startBot = async (cliLogLevel) => {
759
817
  // Auto-launch Antigravity with CDP port if not already running
760
818
  await (0, antigravityLauncher_1.ensureAntigravityRunning)();
761
819
  // Initialize CDP bridge (lazy connection: pool creation only)
762
- const bridge = (0, cdpBridgeManager_1.initCdpBridge)(config.autoApproveFileEdits);
820
+ const accountPorts = Object.fromEntries((config.antigravityAccounts ?? []).map((account) => [account.name, account.cdpPort]));
821
+ const accountUserDataDirs = Object.fromEntries((config.antigravityAccounts ?? [])
822
+ .filter((account) => typeof account.userDataDir === 'string' && account.userDataDir.trim().length > 0)
823
+ .map((account) => [account.name, account.userDataDir.trim()]));
824
+ const bridge = (0, cdpBridgeManager_1.initCdpBridge)(config.autoApproveFileEdits, accountPorts, accountUserDataDirs);
763
825
  // Initialize CDP-dependent services (constructor CDP dependency removed)
764
826
  const chatSessionService = new chatSessionService_1.ChatSessionService();
765
827
  const titleGenerator = new titleGeneratorService_1.TitleGeneratorService();
@@ -770,8 +832,46 @@ const startBot = async (cliLogLevel) => {
770
832
  sendPromptImpl: sendPromptToAntigravity,
771
833
  });
772
834
  // Initialize command handlers (joinHandler is created after client, see below)
773
- const wsHandler = new workspaceCommandHandler_1.WorkspaceCommandHandler(workspaceBindingRepo, chatSessionRepo, workspaceService, channelManager);
774
- const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool);
835
+ const wsHandler = new workspaceCommandHandler_1.WorkspaceCommandHandler(workspaceBindingRepo, chatSessionRepo, workspaceService, channelManager, async (workspaceName, newChannelId, sourceChannelId, userId) => {
836
+ const workspacePath = workspaceService.getWorkspacePath(workspaceName);
837
+ const selectedAccount = (0, accountUtils_1.resolveScopedAccountName)({
838
+ channelId: sourceChannelId,
839
+ userId,
840
+ sessionAccountName: chatSessionRepo.findByChannelId(sourceChannelId)?.activeAccountName ?? null,
841
+ parentChannelId: null,
842
+ selectedAccountByChannel: bridge.selectedAccountByChannel,
843
+ channelPrefRepo,
844
+ accountPrefRepo,
845
+ accounts: config.antigravityAccounts,
846
+ });
847
+ chatSessionRepo.setActiveAccountName(newChannelId, selectedAccount);
848
+ bridge.selectedAccountByChannel?.set(newChannelId, selectedAccount);
849
+ bridge.pool.setPreferredAccountForWorkspace(workspacePath, selectedAccount);
850
+ const cdp = new cdpService_1.CdpService({
851
+ accountName: selectedAccount,
852
+ accountPorts,
853
+ accountUserDataDirs,
854
+ cdpCallTimeout: 15000,
855
+ maxReconnectAttempts: 0,
856
+ });
857
+ try {
858
+ await cdp.openWorkspace(workspacePath);
859
+ }
860
+ finally {
861
+ await cdp.disconnect().catch(() => { });
862
+ }
863
+ await bridge.pool.getOrConnect(workspacePath, { name: selectedAccount });
864
+ });
865
+ const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool, (channelId, userId) => (0, accountUtils_1.resolveScopedAccountName)({
866
+ channelId,
867
+ userId,
868
+ sessionAccountName: chatSessionRepo.findByChannelId(channelId)?.activeAccountName ?? null,
869
+ parentChannelId: null,
870
+ selectedAccountByChannel: bridge.selectedAccountByChannel,
871
+ channelPrefRepo,
872
+ accountPrefRepo,
873
+ accounts: config.antigravityAccounts,
874
+ }));
775
875
  const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
776
876
  const slashCommandHandler = new slashCommandHandler_1.SlashCommandHandler(templateRepo);
777
877
  // Discord platform — only initialise the Discord client when the platform is enabled
@@ -789,7 +889,16 @@ const startBot = async (cliLogLevel) => {
789
889
  discord_js_1.GatewayIntentBits.MessageContent,
790
890
  ]
791
891
  });
792
- const joinHandler = new joinCommandHandler_1.JoinCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, bridge.pool, workspaceService, client, config.extractionMode, config.responseTimeoutMs);
892
+ const joinHandler = new joinCommandHandler_1.JoinCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, bridge.pool, workspaceService, client, config.extractionMode, config.responseTimeoutMs, (channelId, userId) => (0, accountUtils_1.resolveScopedAccountName)({
893
+ channelId,
894
+ userId,
895
+ sessionAccountName: chatSessionRepo.findByChannelId(channelId)?.activeAccountName ?? null,
896
+ parentChannelId: null,
897
+ selectedAccountByChannel: bridge.selectedAccountByChannel,
898
+ channelPrefRepo,
899
+ accountPrefRepo,
900
+ accounts: config.antigravityAccounts,
901
+ }));
793
902
  client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
794
903
  logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
795
904
  try {
@@ -854,7 +963,12 @@ const startBot = async (cliLogLevel) => {
854
963
  parseRunCommandCustomId: cdpBridgeManager_1.parseRunCommandCustomId,
855
964
  joinHandler,
856
965
  userPrefRepo,
857
- 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),
966
+ accountPrefRepo,
967
+ channelPrefRepo,
968
+ chatSessionRepo,
969
+ chatSessionService,
970
+ antigravityAccounts: config.antigravityAccounts,
971
+ handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, chatSessionService, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepo),
858
972
  handleTemplateUse: async (interaction, templateId) => {
859
973
  const template = templateRepo.findById(templateId);
860
974
  if (!template) {
@@ -870,7 +984,18 @@ const startBot = async (cliLogLevel) => {
870
984
  let cdp = null;
871
985
  if (workspacePath) {
872
986
  try {
873
- cdp = await bridge.pool.getOrConnect(workspacePath);
987
+ const selectedAccount = (0, accountUtils_1.resolveScopedAccountName)({
988
+ channelId,
989
+ userId: interaction.user.id,
990
+ sessionAccountName: chatSessionRepo.findByChannelId(channelId)?.activeAccountName ?? null,
991
+ parentChannelId: (0, accountUtils_1.inferParentScopeChannelId)(channelId, interaction.channel?.parentId ?? null),
992
+ selectedAccountByChannel: bridge.selectedAccountByChannel,
993
+ channelPrefRepo,
994
+ accountPrefRepo,
995
+ accounts: config.antigravityAccounts,
996
+ });
997
+ bridge.selectedAccountByChannel?.set(channelId, selectedAccount);
998
+ cdp = await bridge.pool.getOrConnect(workspacePath, { name: selectedAccount });
874
999
  const projectName = bridge.pool.extractProjectName(workspacePath);
875
1000
  bridge.lastActiveWorkspace = projectName;
876
1001
  const platformCh = (0, wrappers_1.wrapDiscordChannel)(interaction.channel);
@@ -880,10 +1005,10 @@ const startBot = async (cliLogLevel) => {
880
1005
  if (session?.displayName) {
881
1006
  (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, session.displayName, platformCh);
882
1007
  }
883
- (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName);
884
- (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName);
885
- (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName);
886
- (0, cdpBridgeManager_1.ensureRunCommandDetector)(bridge, cdp, projectName);
1008
+ (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName, selectedAccount);
1009
+ (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName, selectedAccount);
1010
+ (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName, selectedAccount);
1011
+ (0, cdpBridgeManager_1.ensureRunCommandDetector)(bridge, cdp, projectName, selectedAccount);
887
1012
  }
888
1013
  catch (e) {
889
1014
  await interaction.followUp({
@@ -894,7 +1019,19 @@ const startBot = async (cliLogLevel) => {
894
1019
  }
895
1020
  }
896
1021
  else {
897
- cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1022
+ const selectedAccount = (0, accountUtils_1.resolveScopedAccountName)({
1023
+ channelId,
1024
+ userId: interaction.user.id,
1025
+ sessionAccountName: chatSessionRepo.findByChannelId(channelId)?.activeAccountName ?? null,
1026
+ parentChannelId: (0, accountUtils_1.inferParentScopeChannelId)(channelId, interaction.channel?.parentId ?? null),
1027
+ selectedAccountByChannel: bridge.selectedAccountByChannel,
1028
+ channelPrefRepo,
1029
+ accountPrefRepo,
1030
+ accounts: config.antigravityAccounts,
1031
+ });
1032
+ cdp = bridge.lastActiveWorkspace
1033
+ ? bridge.pool.getConnected(bridge.lastActiveWorkspace, selectedAccount)
1034
+ : null;
898
1035
  }
899
1036
  if (!cdp) {
900
1037
  await interaction.followUp({
@@ -947,6 +1084,9 @@ const startBot = async (cliLogLevel) => {
947
1084
  autoRenameChannel,
948
1085
  handleScreenshot: screenshotUi_1.handleScreenshot,
949
1086
  userPrefRepo,
1087
+ accountPrefRepo,
1088
+ channelPrefRepo,
1089
+ antigravityAccounts: config.antigravityAccounts,
950
1090
  }));
951
1091
  await client.login(discordToken);
952
1092
  } // end: else (credentials present)
@@ -989,21 +1129,55 @@ const startBot = async (cliLogLevel) => {
989
1129
  botApi: telegramBot.api,
990
1130
  chatSessionService,
991
1131
  responseTimeoutMs: config.responseTimeoutMs,
1132
+ accountPrefRepo,
1133
+ channelPrefRepo,
1134
+ antigravityAccounts: config.antigravityAccounts,
992
1135
  });
993
1136
  // Compose select handlers: project select + mode select
994
1137
  const projectSelectHandler = (0, telegramProjectCommand_1.createTelegramSelectHandler)({
1138
+ botApi: telegramBot.api,
1139
+ bridge,
995
1140
  workspaceService,
996
1141
  telegramBindingRepo,
997
1142
  });
998
1143
  const modeSelectAction = (0, modeSelectAction_1.createModeSelectAction)({ bridge, modeService });
1144
+ const accountSelectAction = (0, accountSelectAction_1.createAccountSelectAction)({
1145
+ bridge,
1146
+ accountPrefRepo,
1147
+ channelPrefRepo,
1148
+ chatSessionRepo,
1149
+ antigravityAccounts: config.antigravityAccounts,
1150
+ getWorkspacePathForChannel: (channelId) => {
1151
+ const binding = telegramBindingRepo.findByChatId(channelId);
1152
+ if (!binding)
1153
+ return null;
1154
+ return workspaceService
1155
+ ? workspaceService.getWorkspacePath(binding.workspacePath)
1156
+ : binding.workspacePath;
1157
+ },
1158
+ });
999
1159
  const telegramSelectHandler = (0, selectHandler_1.createPlatformSelectHandler)({
1000
1160
  actions: [
1001
1161
  modeSelectAction,
1162
+ accountSelectAction,
1002
1163
  ],
1003
1164
  });
1004
1165
  // Composite handler that routes to the right handler
1005
1166
  const compositeSelectHandler = async (interaction) => {
1006
- if (interaction.customId === 'mode_select') {
1167
+ if (interaction.customId === sessionPickerUi_1.SESSION_SELECT_ID) {
1168
+ await (0, telegramJoinCommand_1.handleTelegramJoinSelect)({
1169
+ bridge,
1170
+ botApi: telegramBot.api,
1171
+ telegramBindingRepo,
1172
+ workspaceService,
1173
+ chatSessionService,
1174
+ accountPrefRepo,
1175
+ channelPrefRepo,
1176
+ antigravityAccounts: config.antigravityAccounts,
1177
+ }, interaction);
1178
+ return;
1179
+ }
1180
+ if (interaction.customId === 'mode_select' || interaction.customId === 'account_select') {
1007
1181
  await telegramSelectHandler(interaction);
1008
1182
  return;
1009
1183
  }
@@ -1022,7 +1196,41 @@ const startBot = async (cliLogLevel) => {
1022
1196
  (0, planningButtonAction_1.createPlanningButtonAction)({ bridge }),
1023
1197
  (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge }),
1024
1198
  (0, runCommandButtonAction_1.createRunCommandButtonAction)({ bridge }),
1025
- (0, modelButtonAction_1.createModelButtonAction)({ bridge, fetchQuota: () => bridge.quota.fetchQuota(), modelService, userPrefRepo }),
1199
+ (0, modelButtonAction_1.createModelButtonAction)({
1200
+ bridge,
1201
+ fetchQuota: () => bridge.quota.fetchQuota(),
1202
+ modelService,
1203
+ userPrefRepo,
1204
+ ensureSessionActivated: async (channelId, userId, cdp) => {
1205
+ const savedTitle = chatSessionRepo.findByChannelId(channelId)?.displayName?.trim() || '';
1206
+ if (!savedTitle || savedTitle === (0, i18n_1.t)('(Untitled)')) {
1207
+ return { ok: true };
1208
+ }
1209
+ const current = await chatSessionService.getCurrentSessionInfo(cdp);
1210
+ if (current.title.trim() === savedTitle) {
1211
+ return { ok: true };
1212
+ }
1213
+ logger_1.logger.info(`[ModelCommand] source=button channel=${channelId} user=${userId} ` +
1214
+ `restoringSession target="${savedTitle}" current="${current.title.trim() || '(unknown)'}"`);
1215
+ const activation = await chatSessionService.activateSessionByTitle(cdp, savedTitle, {
1216
+ maxWaitMs: 8000,
1217
+ retryIntervalMs: 300,
1218
+ allowVisibilityWarmupMs: 1000,
1219
+ });
1220
+ if (!activation.ok) {
1221
+ return {
1222
+ ok: false,
1223
+ error: `Failed to activate saved session "${savedTitle}" before model action: ${activation.error || 'unknown'}`,
1224
+ };
1225
+ }
1226
+ const refresh = await chatSessionService.refreshSessionViewIfStuck(cdp, savedTitle);
1227
+ if (!refresh.ok) {
1228
+ logger_1.logger.warn(`[ModelCommand] source=button channel=${channelId} user=${userId} ` +
1229
+ `sessionRefreshWarning target="${savedTitle}" error="${refresh.error || 'unknown'}"`);
1230
+ }
1231
+ return { ok: true };
1232
+ },
1233
+ }),
1026
1234
  (0, autoAcceptButtonAction_1.createAutoAcceptButtonAction)({ autoAcceptService: bridge.autoAccept }),
1027
1235
  (0, templateButtonAction_1.createTemplateButtonAction)({ bridge, templateRepo }),
1028
1236
  ],
@@ -1041,11 +1249,14 @@ const startBot = async (cliLogLevel) => {
1041
1249
  { command: 'model', description: 'Switch LLM model' },
1042
1250
  { command: 'screenshot', description: 'Capture Antigravity screenshot' },
1043
1251
  { command: 'autoaccept', description: 'Toggle auto-accept mode' },
1252
+ { command: 'account', description: 'Switch Antigravity account' },
1044
1253
  { command: 'template', description: 'List prompt templates' },
1045
1254
  { command: 'template_add', description: 'Add a prompt template' },
1046
1255
  { command: 'template_delete', description: 'Delete a prompt template' },
1047
1256
  { command: 'project_create', description: 'Create a new workspace' },
1048
1257
  { command: 'new', description: 'Start a new chat session' },
1258
+ { command: 'join', description: 'Take over an existing session' },
1259
+ { command: 'mirror', description: 'Toggle PC-to-Telegram message mirroring' },
1049
1260
  { command: 'logs', description: 'Show recent log entries' },
1050
1261
  { command: 'stop', description: 'Interrupt active LLM generation' },
1051
1262
  { command: 'help', description: 'Show available commands' },
@@ -1056,7 +1267,8 @@ const startBot = async (cliLogLevel) => {
1056
1267
  eventRouter.registerAdapter(telegramAdapter);
1057
1268
  await eventRouter.startAll();
1058
1269
  logger_1.logger.info(`Telegram bot started: @${botInfo.username} (${config.telegramAllowedUserIds?.length ?? 0} allowed users)`);
1059
- // Send startup message to all bound Telegram chats
1270
+ // Send startup message to one Telegram target:
1271
+ // prefer a group named "general", otherwise the first private chat.
1060
1272
  const bindings = telegramBindingRepo.findAll();
1061
1273
  if (bindings.length > 0) {
1062
1274
  const os = await Promise.resolve().then(() => __importStar(require('os')));
@@ -1098,13 +1310,15 @@ const startBot = async (cliLogLevel) => {
1098
1310
  }
1099
1311
  }
1100
1312
  };
1101
- const results = await Promise.allSettled(bindings.map((binding) => sendWithRetry(binding.chatId, startupText)));
1102
- const failed = results.filter((r) => r.status === 'rejected');
1103
- if (failed.length > 0) {
1104
- logger_1.logger.warn(`[Telegram] Startup message failed for ${failed.length}/${bindings.length} chat(s) after retries: ${failed[0].reason?.message ?? 'unknown error'}`);
1105
- }
1106
- else {
1107
- logger_1.logger.info(`Telegram startup message sent to ${bindings.length} bound chat(s).`);
1313
+ const targetChatId = await (0, telegramStartupTarget_1.selectTelegramStartupChatId)(telegramBot.api, bindings);
1314
+ if (targetChatId) {
1315
+ try {
1316
+ await sendWithRetry(targetChatId, startupText);
1317
+ logger_1.logger.info(`Telegram startup message sent to chat ${targetChatId}.`);
1318
+ }
1319
+ catch (error) {
1320
+ logger_1.logger.warn(`[Telegram] Startup message failed for chat ${targetChatId} after retries: ${error?.message ?? 'unknown error'}`);
1321
+ }
1108
1322
  }
1109
1323
  }
1110
1324
  }
@@ -1138,8 +1352,78 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
1138
1352
  /**
1139
1353
  * Handle Discord Interactions API slash commands
1140
1354
  */
1141
- async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo) {
1355
+ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, chatSessionService, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepo, channelPrefRepo, antigravityAccounts = [{ name: 'default', cdpPort: 9222 }], chatSessionRepo) {
1142
1356
  const commandName = interaction.commandName;
1357
+ const getAccountPort = (accountName) => {
1358
+ const match = antigravityAccounts.find((account) => account.name === accountName);
1359
+ return match ? match.cdpPort : null;
1360
+ };
1361
+ const parentChannelId = (0, accountUtils_1.inferParentScopeChannelId)(interaction.channelId, interaction.channel?.parentId ?? null);
1362
+ const getSessionAccountName = () => chatSessionRepo?.findByChannelId(interaction.channelId)?.activeAccountName ?? null;
1363
+ const resolveSelectedAccount = () => (0, accountUtils_1.resolveScopedAccountName)({
1364
+ channelId: interaction.channelId,
1365
+ userId: interaction.user.id,
1366
+ sessionAccountName: getSessionAccountName(),
1367
+ parentChannelId,
1368
+ selectedAccountByChannel: bridge.selectedAccountByChannel,
1369
+ channelPrefRepo,
1370
+ accountPrefRepo,
1371
+ accounts: antigravityAccounts,
1372
+ });
1373
+ const getChannelWorkspacePath = () => wsHandler.getWorkspaceForChannel(interaction.channelId);
1374
+ const getChannelCdp = () => (() => {
1375
+ const workspacePath = getChannelWorkspacePath();
1376
+ if (workspacePath) {
1377
+ const projectName = bridge.pool.extractProjectName(workspacePath);
1378
+ return bridge.pool.getConnected(projectName, resolveSelectedAccount());
1379
+ }
1380
+ return bridge.lastActiveWorkspace
1381
+ ? bridge.pool.getConnected(bridge.lastActiveWorkspace, resolveSelectedAccount())
1382
+ : null;
1383
+ })();
1384
+ const ensureChannelCdp = async () => {
1385
+ const existing = getChannelCdp();
1386
+ if (existing)
1387
+ return existing;
1388
+ const workspacePath = getChannelWorkspacePath();
1389
+ if (!workspacePath)
1390
+ return null;
1391
+ try {
1392
+ return await bridge.pool.getOrConnect(workspacePath, { name: resolveSelectedAccount() });
1393
+ }
1394
+ catch {
1395
+ return null;
1396
+ }
1397
+ };
1398
+ const ensureBoundSessionActive = async (cdp) => {
1399
+ const savedTitle = chatSessionRepo?.findByChannelId(interaction.channelId)?.displayName?.trim() || '';
1400
+ if (!savedTitle || savedTitle === (0, i18n_1.t)('(Untitled)')) {
1401
+ return { ok: true };
1402
+ }
1403
+ const current = await chatSessionService.getCurrentSessionInfo(cdp);
1404
+ if (current.title.trim() === savedTitle) {
1405
+ return { ok: true };
1406
+ }
1407
+ logger_1.logger.info(`[ModelCommand] source=slash channel=${interaction.channelId} user=${interaction.user.id} ` +
1408
+ `restoringSession target="${savedTitle}" current="${current.title.trim() || '(unknown)'}"`);
1409
+ const activation = await chatSessionService.activateSessionByTitle(cdp, savedTitle, {
1410
+ maxWaitMs: 8000,
1411
+ retryIntervalMs: 300,
1412
+ allowVisibilityWarmupMs: 1000,
1413
+ });
1414
+ if (!activation.ok) {
1415
+ return {
1416
+ ok: false,
1417
+ error: `Failed to activate saved session "${savedTitle}" before model action: ${activation.error || 'unknown'}`,
1418
+ };
1419
+ }
1420
+ const refresh = await chatSessionService.refreshSessionViewIfStuck(cdp, savedTitle);
1421
+ if (!refresh.ok) {
1422
+ logger_1.logger.warn(`[ModelCommand] source=slash channel=${interaction.channelId} user=${interaction.user.id} ` +
1423
+ `sessionRefreshWarning target="${savedTitle}" error="${refresh.error || 'unknown'}"`);
1424
+ }
1425
+ return { ok: true };
1426
+ };
1143
1427
  switch (commandName) {
1144
1428
  case 'help': {
1145
1429
  const helpFields = [
@@ -1172,6 +1456,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1172
1456
  name: '📁 Projects', value: [
1173
1457
  '`/project` — Display project list',
1174
1458
  '`/project create <name>` — Create a new project',
1459
+ '`/project account [name]` — Show or change the project channel account',
1175
1460
  ].join('\n')
1176
1461
  },
1177
1462
  {
@@ -1185,6 +1470,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1185
1470
  name: '🔧 System', value: [
1186
1471
  '`/status` — Display overall bot status',
1187
1472
  '`/autoaccept` — Toggle auto-approve mode for approval dialogs via buttons',
1473
+ '`/account` — Show and switch Antigravity account',
1188
1474
  '`/logs [lines] [level]` — View recent bot logs',
1189
1475
  '`/cleanup [days]` — Clean up unused channels/categories',
1190
1476
  '`/help` — Show this help',
@@ -1213,24 +1499,47 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1213
1499
  break;
1214
1500
  }
1215
1501
  case 'mode': {
1216
- await (0, modeUi_1.sendModeUI)(interaction, modeService, { getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge) });
1502
+ await (0, modeUi_1.sendModeUI)(interaction, modeService, { getCurrentCdp: () => getChannelCdp() });
1217
1503
  break;
1218
1504
  }
1219
1505
  case 'model': {
1220
1506
  const modelName = interaction.options.getString('name');
1507
+ logger_1.logger.info(`[ModelCommand] source=slash channel=${interaction.channelId} user=${interaction.user.id} ` +
1508
+ `requested=${modelName ? `"${modelName}"` : 'ui'}`);
1221
1509
  if (!modelName) {
1510
+ const cdp = await ensureChannelCdp();
1511
+ if (!cdp) {
1512
+ logger_1.logger.warn(`[ModelCommand] source=slash channel=${interaction.channelId} user=${interaction.user.id} cdp=unavailable`);
1513
+ await interaction.editReply({ content: 'Not connected to CDP.' });
1514
+ break;
1515
+ }
1516
+ const sessionReady = await ensureBoundSessionActive(cdp);
1517
+ if (!sessionReady.ok) {
1518
+ await interaction.editReply({ content: sessionReady.error });
1519
+ break;
1520
+ }
1222
1521
  await (0, modelsUi_1.sendModelsUI)(interaction, {
1223
- getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge),
1522
+ getCurrentCdp: () => cdp,
1224
1523
  fetchQuota: async () => bridge.quota.fetchQuota(),
1225
1524
  });
1226
1525
  }
1227
1526
  else {
1228
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1527
+ const cdp = await ensureChannelCdp();
1229
1528
  if (!cdp) {
1529
+ logger_1.logger.warn(`[ModelCommand] source=slash channel=${interaction.channelId} user=${interaction.user.id} target="${modelName}" cdp=unavailable`);
1230
1530
  await interaction.editReply({ content: 'Not connected to CDP.' });
1231
1531
  break;
1232
1532
  }
1533
+ const sessionReady = await ensureBoundSessionActive(cdp);
1534
+ if (!sessionReady.ok) {
1535
+ await interaction.editReply({ content: sessionReady.error });
1536
+ break;
1537
+ }
1233
1538
  const res = await cdp.setUiModel(modelName);
1539
+ logger_1.logger.info(`[ModelCommand] source=slash channel=${interaction.channelId} user=${interaction.user.id} ` +
1540
+ `target="${modelName}" ok=${res.ok} applied=${res.model ? `"${res.model}"` : 'null'} ` +
1541
+ `verified=${res.verified === true} alreadySelected=${res.alreadySelected === true} ` +
1542
+ `error=${res.error ? `"${res.error}"` : 'null'}`);
1234
1543
  if (res.ok) {
1235
1544
  await interaction.editReply({ content: `Model changed to **${res.model}**.` });
1236
1545
  }
@@ -1270,19 +1579,26 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1270
1579
  case 'status': {
1271
1580
  const activeNames = bridge.pool.getActiveWorkspaceNames();
1272
1581
  const currentModel = (() => {
1273
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1582
+ const cdp = getChannelCdp();
1274
1583
  return cdp ? 'CDP Connected' : 'Disconnected';
1275
1584
  })();
1276
1585
  const currentMode = modeService.getCurrentMode();
1586
+ const session = chatSessionRepo?.findByChannelId(interaction.channelId);
1277
1587
  const mirroringWorkspaces = activeNames.filter((name) => bridge.pool.getUserMessageDetector(name)?.isActive());
1278
1588
  const mirrorStatus = mirroringWorkspaces.length > 0
1279
1589
  ? `📡 ON (${mirroringWorkspaces.join(', ')})`
1280
1590
  : '⚪ OFF';
1591
+ const currentAccount = resolveSelectedAccount();
1592
+ const originalAccount = session?.originAccountName ?? '(unset)';
1593
+ const conversationTitle = session?.displayName ?? '(New chat / no saved title)';
1281
1594
  const statusFields = [
1282
1595
  { name: 'CDP Connection', value: activeNames.length > 0 ? `🟢 ${activeNames.length} project(s) connected` : '⚪ Disconnected', inline: true },
1283
1596
  { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true },
1284
1597
  { name: 'Auto Approve', value: autoAcceptService.isEnabled() ? '🟢 ON' : '⚪ OFF', inline: true },
1285
1598
  { name: 'Mirroring', value: mirrorStatus, inline: true },
1599
+ { name: 'Active Account', value: currentAccount, inline: true },
1600
+ { name: 'Original Account', value: originalAccount, inline: true },
1601
+ { name: 'Conversation Title', value: conversationTitle, inline: false },
1286
1602
  ];
1287
1603
  let statusDescription = '';
1288
1604
  if (activeNames.length > 0) {
@@ -1327,6 +1643,38 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1327
1643
  await interaction.editReply({ content: result.message });
1328
1644
  break;
1329
1645
  }
1646
+ case 'account': {
1647
+ if (!accountPrefRepo) {
1648
+ await interaction.editReply({ content: 'Account preference service not available.' });
1649
+ break;
1650
+ }
1651
+ const requested = interaction.options.getString('name');
1652
+ if (!requested) {
1653
+ const current = resolveSelectedAccount();
1654
+ const names = (0, accountUtils_1.listAccountNames)(antigravityAccounts);
1655
+ await (0, accountUi_1.sendAccountUI)(interaction, current, names);
1656
+ break;
1657
+ }
1658
+ if (!(0, accountUtils_1.listAccountNames)(antigravityAccounts).includes(requested)) {
1659
+ await interaction.editReply({ content: `⚠️ Unknown account: **${requested}**` });
1660
+ break;
1661
+ }
1662
+ bridge.selectedAccountByChannel?.set(interaction.channelId, requested);
1663
+ const currentSession = chatSessionRepo?.findByChannelId(interaction.channelId);
1664
+ if (currentSession) {
1665
+ chatSessionRepo?.setActiveAccountName(interaction.channelId, requested);
1666
+ }
1667
+ else {
1668
+ accountPrefRepo.setAccountName(interaction.user.id, requested);
1669
+ channelPrefRepo?.setAccountName(interaction.channelId, requested);
1670
+ }
1671
+ const channelWorkspace = wsHandler.getWorkspaceForChannel(interaction.channelId);
1672
+ logger_1.logger.info(`[AccountSwitch] source=slash channel=${interaction.channelId} user=${interaction.user.id} ` +
1673
+ `account=${requested} port=${getAccountPort(requested) ?? 'unknown'} ` +
1674
+ `workspace=${channelWorkspace ?? 'unbound'}`);
1675
+ await interaction.editReply({ content: `✅ Switched session account to **${requested}**.` });
1676
+ break;
1677
+ }
1330
1678
  case 'output': {
1331
1679
  if (!userPrefRepo) {
1332
1680
  await interaction.editReply({ content: 'Output preference service not available.' });
@@ -1345,11 +1693,11 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1345
1693
  break;
1346
1694
  }
1347
1695
  case 'screenshot': {
1348
- await (0, screenshotUi_1.handleScreenshot)(interaction, (0, cdpBridgeManager_1.getCurrentCdp)(bridge));
1696
+ await (0, screenshotUi_1.handleScreenshot)(interaction, getChannelCdp());
1349
1697
  break;
1350
1698
  }
1351
1699
  case 'stop': {
1352
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1700
+ const cdp = getChannelCdp();
1353
1701
  if (!cdp) {
1354
1702
  await interaction.editReply({ content: '⚠️ Not connected to CDP. Please connect to a project first.' });
1355
1703
  break;
@@ -1398,6 +1746,29 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1398
1746
  }
1399
1747
  await wsHandler.handleCreate(interaction, interaction.guild);
1400
1748
  }
1749
+ else if (wsSub === 'account') {
1750
+ const requested = interaction.options.getString('name');
1751
+ const names = (0, accountUtils_1.listAccountNames)(antigravityAccounts);
1752
+ const currentProjectAccount = channelPrefRepo?.getAccountName(interaction.channelId) ?? null;
1753
+ if (!requested) {
1754
+ await interaction.editReply({
1755
+ content: `Project channel account: **${currentProjectAccount ?? 'unset'}**\nAvailable: ${names.join(', ')}`,
1756
+ });
1757
+ break;
1758
+ }
1759
+ if (!names.includes(requested)) {
1760
+ await interaction.editReply({ content: `⚠️ Unknown account: **${requested}**` });
1761
+ break;
1762
+ }
1763
+ channelPrefRepo?.setAccountName(interaction.channelId, requested);
1764
+ bridge.selectedAccountByChannel?.set(interaction.channelId, requested);
1765
+ const channelWorkspace = wsHandler.getWorkspaceForChannel(interaction.channelId);
1766
+ logger_1.logger.info(`[ProjectAccountSwitch] source=slash channel=${interaction.channelId} user=${interaction.user.id} ` +
1767
+ `account=${requested} port=${getAccountPort(requested) ?? 'unknown'} ` +
1768
+ `workspace=${channelWorkspace ?? 'unbound'}`);
1769
+ await interaction.editReply({ content: `✅ Bound this project channel to account **${requested}**.` });
1770
+ break;
1771
+ }
1401
1772
  else {
1402
1773
  // /project list or /project (default)
1403
1774
  await wsHandler.handleShow(interaction);