lazy-gravity 0.6.2 → 0.7.1

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.
@@ -786,6 +797,8 @@ const startBot = async (cliLogLevel) => {
786
797
  const modelService = new modelService_1.ModelService();
787
798
  const templateRepo = new templateRepository_1.TemplateRepository(db);
788
799
  const userPrefRepo = new userPreferenceRepository_1.UserPreferenceRepository(db);
800
+ const accountPrefRepo = new accountPreferenceRepository_1.AccountPreferenceRepository(db);
801
+ const channelPrefRepo = new channelPreferenceRepository_1.ChannelPreferenceRepository(db);
789
802
  // Eagerly load default model from DB (single-user bot optimization)
790
803
  try {
791
804
  const firstUser = db.prepare('SELECT user_id FROM user_preferences LIMIT 1').get();
@@ -804,7 +817,11 @@ const startBot = async (cliLogLevel) => {
804
817
  // Auto-launch Antigravity with CDP port if not already running
805
818
  await (0, antigravityLauncher_1.ensureAntigravityRunning)();
806
819
  // Initialize CDP bridge (lazy connection: pool creation only)
807
- 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);
808
825
  // Initialize CDP-dependent services (constructor CDP dependency removed)
809
826
  const chatSessionService = new chatSessionService_1.ChatSessionService();
810
827
  const titleGenerator = new titleGeneratorService_1.TitleGeneratorService();
@@ -815,8 +832,46 @@ const startBot = async (cliLogLevel) => {
815
832
  sendPromptImpl: sendPromptToAntigravity,
816
833
  });
817
834
  // Initialize command handlers (joinHandler is created after client, see below)
818
- const wsHandler = new workspaceCommandHandler_1.WorkspaceCommandHandler(workspaceBindingRepo, chatSessionRepo, workspaceService, channelManager);
819
- 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
+ }));
820
875
  const cleanupHandler = new cleanupCommandHandler_1.CleanupCommandHandler(chatSessionRepo, workspaceBindingRepo);
821
876
  const slashCommandHandler = new slashCommandHandler_1.SlashCommandHandler(templateRepo);
822
877
  // Discord platform — only initialise the Discord client when the platform is enabled
@@ -834,7 +889,16 @@ const startBot = async (cliLogLevel) => {
834
889
  discord_js_1.GatewayIntentBits.MessageContent,
835
890
  ]
836
891
  });
837
- 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
+ }));
838
902
  client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
839
903
  logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
840
904
  try {
@@ -899,7 +963,12 @@ const startBot = async (cliLogLevel) => {
899
963
  parseRunCommandCustomId: cdpBridgeManager_1.parseRunCommandCustomId,
900
964
  joinHandler,
901
965
  userPrefRepo,
902
- 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),
903
972
  handleTemplateUse: async (interaction, templateId) => {
904
973
  const template = templateRepo.findById(templateId);
905
974
  if (!template) {
@@ -915,7 +984,18 @@ const startBot = async (cliLogLevel) => {
915
984
  let cdp = null;
916
985
  if (workspacePath) {
917
986
  try {
918
- 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 });
919
999
  const projectName = bridge.pool.extractProjectName(workspacePath);
920
1000
  bridge.lastActiveWorkspace = projectName;
921
1001
  const platformCh = (0, wrappers_1.wrapDiscordChannel)(interaction.channel);
@@ -925,10 +1005,10 @@ const startBot = async (cliLogLevel) => {
925
1005
  if (session?.displayName) {
926
1006
  (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, projectName, session.displayName, platformCh);
927
1007
  }
928
- (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName);
929
- (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName);
930
- (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName);
931
- (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);
932
1012
  }
933
1013
  catch (e) {
934
1014
  await interaction.followUp({
@@ -939,7 +1019,19 @@ const startBot = async (cliLogLevel) => {
939
1019
  }
940
1020
  }
941
1021
  else {
942
- 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;
943
1035
  }
944
1036
  if (!cdp) {
945
1037
  await interaction.followUp({
@@ -992,6 +1084,9 @@ const startBot = async (cliLogLevel) => {
992
1084
  autoRenameChannel,
993
1085
  handleScreenshot: screenshotUi_1.handleScreenshot,
994
1086
  userPrefRepo,
1087
+ accountPrefRepo,
1088
+ channelPrefRepo,
1089
+ antigravityAccounts: config.antigravityAccounts,
995
1090
  }));
996
1091
  await client.login(discordToken);
997
1092
  } // end: else (credentials present)
@@ -1034,21 +1129,55 @@ const startBot = async (cliLogLevel) => {
1034
1129
  botApi: telegramBot.api,
1035
1130
  chatSessionService,
1036
1131
  responseTimeoutMs: config.responseTimeoutMs,
1132
+ accountPrefRepo,
1133
+ channelPrefRepo,
1134
+ antigravityAccounts: config.antigravityAccounts,
1037
1135
  });
1038
1136
  // Compose select handlers: project select + mode select
1039
1137
  const projectSelectHandler = (0, telegramProjectCommand_1.createTelegramSelectHandler)({
1138
+ botApi: telegramBot.api,
1139
+ bridge,
1040
1140
  workspaceService,
1041
1141
  telegramBindingRepo,
1042
1142
  });
1043
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
+ });
1044
1159
  const telegramSelectHandler = (0, selectHandler_1.createPlatformSelectHandler)({
1045
1160
  actions: [
1046
1161
  modeSelectAction,
1162
+ accountSelectAction,
1047
1163
  ],
1048
1164
  });
1049
1165
  // Composite handler that routes to the right handler
1050
1166
  const compositeSelectHandler = async (interaction) => {
1051
- 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') {
1052
1181
  await telegramSelectHandler(interaction);
1053
1182
  return;
1054
1183
  }
@@ -1067,7 +1196,41 @@ const startBot = async (cliLogLevel) => {
1067
1196
  (0, planningButtonAction_1.createPlanningButtonAction)({ bridge }),
1068
1197
  (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge }),
1069
1198
  (0, runCommandButtonAction_1.createRunCommandButtonAction)({ bridge }),
1070
- (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
+ }),
1071
1234
  (0, autoAcceptButtonAction_1.createAutoAcceptButtonAction)({ autoAcceptService: bridge.autoAccept }),
1072
1235
  (0, templateButtonAction_1.createTemplateButtonAction)({ bridge, templateRepo }),
1073
1236
  ],
@@ -1086,11 +1249,14 @@ const startBot = async (cliLogLevel) => {
1086
1249
  { command: 'model', description: 'Switch LLM model' },
1087
1250
  { command: 'screenshot', description: 'Capture Antigravity screenshot' },
1088
1251
  { command: 'autoaccept', description: 'Toggle auto-accept mode' },
1252
+ { command: 'account', description: 'Switch Antigravity account' },
1089
1253
  { command: 'template', description: 'List prompt templates' },
1090
1254
  { command: 'template_add', description: 'Add a prompt template' },
1091
1255
  { command: 'template_delete', description: 'Delete a prompt template' },
1092
1256
  { command: 'project_create', description: 'Create a new workspace' },
1093
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' },
1094
1260
  { command: 'logs', description: 'Show recent log entries' },
1095
1261
  { command: 'stop', description: 'Interrupt active LLM generation' },
1096
1262
  { command: 'help', description: 'Show available commands' },
@@ -1101,7 +1267,8 @@ const startBot = async (cliLogLevel) => {
1101
1267
  eventRouter.registerAdapter(telegramAdapter);
1102
1268
  await eventRouter.startAll();
1103
1269
  logger_1.logger.info(`Telegram bot started: @${botInfo.username} (${config.telegramAllowedUserIds?.length ?? 0} allowed users)`);
1104
- // 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.
1105
1272
  const bindings = telegramBindingRepo.findAll();
1106
1273
  if (bindings.length > 0) {
1107
1274
  const os = await Promise.resolve().then(() => __importStar(require('os')));
@@ -1143,13 +1310,15 @@ const startBot = async (cliLogLevel) => {
1143
1310
  }
1144
1311
  }
1145
1312
  };
1146
- const results = await Promise.allSettled(bindings.map((binding) => sendWithRetry(binding.chatId, startupText)));
1147
- const failed = results.filter((r) => r.status === 'rejected');
1148
- if (failed.length > 0) {
1149
- logger_1.logger.warn(`[Telegram] Startup message failed for ${failed.length}/${bindings.length} chat(s) after retries: ${failed[0].reason?.message ?? 'unknown error'}`);
1150
- }
1151
- else {
1152
- 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
+ }
1153
1322
  }
1154
1323
  }
1155
1324
  }
@@ -1183,8 +1352,78 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
1183
1352
  /**
1184
1353
  * Handle Discord Interactions API slash commands
1185
1354
  */
1186
- 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) {
1187
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
+ };
1188
1427
  switch (commandName) {
1189
1428
  case 'help': {
1190
1429
  const helpFields = [
@@ -1217,6 +1456,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1217
1456
  name: '📁 Projects', value: [
1218
1457
  '`/project` — Display project list',
1219
1458
  '`/project create <name>` — Create a new project',
1459
+ '`/project account [name]` — Show or change the project channel account',
1220
1460
  ].join('\n')
1221
1461
  },
1222
1462
  {
@@ -1230,6 +1470,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1230
1470
  name: '🔧 System', value: [
1231
1471
  '`/status` — Display overall bot status',
1232
1472
  '`/autoaccept` — Toggle auto-approve mode for approval dialogs via buttons',
1473
+ '`/account` — Show and switch Antigravity account',
1233
1474
  '`/logs [lines] [level]` — View recent bot logs',
1234
1475
  '`/cleanup [days]` — Clean up unused channels/categories',
1235
1476
  '`/help` — Show this help',
@@ -1258,24 +1499,47 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1258
1499
  break;
1259
1500
  }
1260
1501
  case 'mode': {
1261
- await (0, modeUi_1.sendModeUI)(interaction, modeService, { getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge) });
1502
+ await (0, modeUi_1.sendModeUI)(interaction, modeService, { getCurrentCdp: () => getChannelCdp() });
1262
1503
  break;
1263
1504
  }
1264
1505
  case 'model': {
1265
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'}`);
1266
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
+ }
1267
1521
  await (0, modelsUi_1.sendModelsUI)(interaction, {
1268
- getCurrentCdp: () => (0, cdpBridgeManager_1.getCurrentCdp)(bridge),
1522
+ getCurrentCdp: () => cdp,
1269
1523
  fetchQuota: async () => bridge.quota.fetchQuota(),
1270
1524
  });
1271
1525
  }
1272
1526
  else {
1273
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1527
+ const cdp = await ensureChannelCdp();
1274
1528
  if (!cdp) {
1529
+ logger_1.logger.warn(`[ModelCommand] source=slash channel=${interaction.channelId} user=${interaction.user.id} target="${modelName}" cdp=unavailable`);
1275
1530
  await interaction.editReply({ content: 'Not connected to CDP.' });
1276
1531
  break;
1277
1532
  }
1533
+ const sessionReady = await ensureBoundSessionActive(cdp);
1534
+ if (!sessionReady.ok) {
1535
+ await interaction.editReply({ content: sessionReady.error });
1536
+ break;
1537
+ }
1278
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'}`);
1279
1543
  if (res.ok) {
1280
1544
  await interaction.editReply({ content: `Model changed to **${res.model}**.` });
1281
1545
  }
@@ -1315,19 +1579,26 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1315
1579
  case 'status': {
1316
1580
  const activeNames = bridge.pool.getActiveWorkspaceNames();
1317
1581
  const currentModel = (() => {
1318
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1582
+ const cdp = getChannelCdp();
1319
1583
  return cdp ? 'CDP Connected' : 'Disconnected';
1320
1584
  })();
1321
1585
  const currentMode = modeService.getCurrentMode();
1586
+ const session = chatSessionRepo?.findByChannelId(interaction.channelId);
1322
1587
  const mirroringWorkspaces = activeNames.filter((name) => bridge.pool.getUserMessageDetector(name)?.isActive());
1323
1588
  const mirrorStatus = mirroringWorkspaces.length > 0
1324
1589
  ? `📡 ON (${mirroringWorkspaces.join(', ')})`
1325
1590
  : '⚪ OFF';
1591
+ const currentAccount = resolveSelectedAccount();
1592
+ const originalAccount = session?.originAccountName ?? '(unset)';
1593
+ const conversationTitle = session?.displayName ?? '(New chat / no saved title)';
1326
1594
  const statusFields = [
1327
1595
  { name: 'CDP Connection', value: activeNames.length > 0 ? `🟢 ${activeNames.length} project(s) connected` : '⚪ Disconnected', inline: true },
1328
1596
  { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true },
1329
1597
  { name: 'Auto Approve', value: autoAcceptService.isEnabled() ? '🟢 ON' : '⚪ OFF', inline: true },
1330
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 },
1331
1602
  ];
1332
1603
  let statusDescription = '';
1333
1604
  if (activeNames.length > 0) {
@@ -1372,6 +1643,38 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1372
1643
  await interaction.editReply({ content: result.message });
1373
1644
  break;
1374
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
+ }
1375
1678
  case 'output': {
1376
1679
  if (!userPrefRepo) {
1377
1680
  await interaction.editReply({ content: 'Output preference service not available.' });
@@ -1390,11 +1693,11 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1390
1693
  break;
1391
1694
  }
1392
1695
  case 'screenshot': {
1393
- await (0, screenshotUi_1.handleScreenshot)(interaction, (0, cdpBridgeManager_1.getCurrentCdp)(bridge));
1696
+ await (0, screenshotUi_1.handleScreenshot)(interaction, getChannelCdp());
1394
1697
  break;
1395
1698
  }
1396
1699
  case 'stop': {
1397
- const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(bridge);
1700
+ const cdp = getChannelCdp();
1398
1701
  if (!cdp) {
1399
1702
  await interaction.editReply({ content: '⚠️ Not connected to CDP. Please connect to a project first.' });
1400
1703
  break;
@@ -1443,6 +1746,29 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1443
1746
  }
1444
1747
  await wsHandler.handleCreate(interaction, interaction.guild);
1445
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
+ }
1446
1772
  else {
1447
1773
  // /project list or /project (default)
1448
1774
  await wsHandler.handleShow(interaction);