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.
@@ -12,8 +12,92 @@ const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
12
12
  const projectListUi_1 = require("../ui/projectListUi");
13
13
  const modeService_1 = require("../services/modeService");
14
14
  const sessionPickerUi_1 = require("../ui/sessionPickerUi");
15
+ const accountUtils_1 = require("../utils/accountUtils");
16
+ const accountUi_1 = require("../ui/accountUi");
15
17
  function createInteractionCreateHandler(deps) {
18
+ const getParentChannelId = (interaction) => (0, accountUtils_1.inferParentScopeChannelId)(interaction.channelId, interaction.channel?.parentId ?? null);
19
+ const getSessionAccountName = (channelId) => deps.chatSessionRepo?.findByChannelId(channelId)?.activeAccountName ?? null;
20
+ const resolveSelectedAccount = (channelId, userId, parentChannelId) => (0, accountUtils_1.resolveScopedAccountName)({
21
+ channelId,
22
+ userId,
23
+ sessionAccountName: getSessionAccountName(channelId),
24
+ parentChannelId,
25
+ selectedAccountByChannel: deps.bridge.selectedAccountByChannel,
26
+ channelPrefRepo: deps.channelPrefRepo,
27
+ accountPrefRepo: deps.accountPrefRepo,
28
+ accounts: deps.antigravityAccounts,
29
+ });
30
+ const getChannelCdp = (channelId, userId) => (() => {
31
+ const workspacePath = deps.wsHandler.getWorkspaceForChannel(channelId);
32
+ if (workspacePath) {
33
+ const projectName = deps.bridge.pool.extractProjectName(workspacePath);
34
+ return deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(channelId, userId));
35
+ }
36
+ return deps.bridge.lastActiveWorkspace
37
+ ? deps.bridge.pool.getConnected(deps.bridge.lastActiveWorkspace, resolveSelectedAccount(channelId, userId))
38
+ : null;
39
+ })();
40
+ const ensureBoundSessionActive = async (channelId, userId, cdp) => {
41
+ const savedTitle = deps.chatSessionRepo?.findByChannelId(channelId)?.displayName?.trim() || '';
42
+ if (!savedTitle || savedTitle === (0, i18n_1.t)('(Untitled)') || !deps.chatSessionService) {
43
+ return { ok: true };
44
+ }
45
+ const current = await deps.chatSessionService.getCurrentSessionInfo(cdp);
46
+ if (current.title.trim() === savedTitle) {
47
+ return { ok: true };
48
+ }
49
+ logger_1.logger.info(`[ModelCommand] source=button channel=${channelId} user=${userId} ` +
50
+ `restoringSession target="${savedTitle}" current="${current.title.trim() || '(unknown)'}"`);
51
+ const activation = await deps.chatSessionService.activateSessionByTitle(cdp, savedTitle, {
52
+ maxWaitMs: 8000,
53
+ retryIntervalMs: 300,
54
+ allowVisibilityWarmupMs: 1000,
55
+ });
56
+ if (!activation.ok) {
57
+ return {
58
+ ok: false,
59
+ error: `Failed to activate saved session "${savedTitle}" before model action: ${activation.error || 'unknown'}`,
60
+ };
61
+ }
62
+ const refresh = await deps.chatSessionService.refreshSessionViewIfStuck(cdp, savedTitle);
63
+ if (!refresh.ok) {
64
+ logger_1.logger.warn(`[ModelCommand] source=button channel=${channelId} user=${userId} ` +
65
+ `sessionRefreshWarning target="${savedTitle}" error="${refresh.error || 'unknown'}"`);
66
+ }
67
+ return { ok: true };
68
+ };
16
69
  return async (interaction) => {
70
+ if (interaction.isAutocomplete()) {
71
+ if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
72
+ await interaction.respond([]).catch(logger_1.logger.error);
73
+ return;
74
+ }
75
+ try {
76
+ if (interaction.commandName === 'project') {
77
+ const subcommand = interaction.options.getSubcommand(false);
78
+ const focused = interaction.options.getFocused(true);
79
+ if (subcommand === 'account' && focused.name === 'name') {
80
+ const names = (0, accountUtils_1.listAccountNames)(deps.antigravityAccounts);
81
+ const currentAccount = resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction));
82
+ const needle = String(focused.value || '').trim().toLowerCase();
83
+ const choices = names
84
+ .filter((name) => !needle || name.toLowerCase().includes(needle))
85
+ .slice(0, 25)
86
+ .map((name) => ({
87
+ name: name === currentAccount ? `${name} (current)` : name,
88
+ value: name,
89
+ }));
90
+ await interaction.respond(choices);
91
+ return;
92
+ }
93
+ }
94
+ }
95
+ catch (error) {
96
+ logger_1.logger.error('Autocomplete handling error:', error);
97
+ }
98
+ await interaction.respond([]).catch(logger_1.logger.error);
99
+ return;
100
+ }
17
101
  if (interaction.isButton()) {
18
102
  if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
19
103
  await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
@@ -31,7 +115,7 @@ function createInteractionCreateHandler(deps) {
31
115
  }
32
116
  const projectName = approvalAction.projectName ?? deps.bridge.lastActiveWorkspace;
33
117
  const detector = projectName
34
- ? deps.bridge.pool.getApprovalDetector(projectName)
118
+ ? deps.bridge.pool.getApprovalDetector(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
35
119
  : undefined;
36
120
  if (!detector) {
37
121
  try {
@@ -101,7 +185,7 @@ function createInteractionCreateHandler(deps) {
101
185
  }
102
186
  const planWorkspaceDirName = planningAction.projectName ?? deps.bridge.lastActiveWorkspace;
103
187
  const planDetector = planWorkspaceDirName
104
- ? deps.bridge.pool.getPlanningDetector(planWorkspaceDirName)
188
+ ? deps.bridge.pool.getPlanningDetector(planWorkspaceDirName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
105
189
  : undefined;
106
190
  if (!planDetector) {
107
191
  try {
@@ -227,7 +311,7 @@ function createInteractionCreateHandler(deps) {
227
311
  }
228
312
  const errorWorkspaceDirName = errorPopupAction.projectName ?? deps.bridge.lastActiveWorkspace;
229
313
  const errorDetector = errorWorkspaceDirName
230
- ? deps.bridge.pool.getErrorPopupDetector(errorWorkspaceDirName)
314
+ ? deps.bridge.pool.getErrorPopupDetector(errorWorkspaceDirName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
231
315
  : undefined;
232
316
  if (!errorDetector) {
233
317
  try {
@@ -377,7 +461,7 @@ function createInteractionCreateHandler(deps) {
377
461
  }
378
462
  const runCmdWorkspace = runCommandAction.projectName ?? deps.bridge.lastActiveWorkspace;
379
463
  const runCmdDetector = runCmdWorkspace
380
- ? deps.bridge.pool.getRunCommandDetector(runCmdWorkspace)
464
+ ? deps.bridge.pool.getRunCommandDetector(runCmdWorkspace, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
381
465
  : undefined;
382
466
  if (!runCmdDetector) {
383
467
  try {
@@ -446,11 +530,16 @@ function createInteractionCreateHandler(deps) {
446
530
  }
447
531
  if (interaction.customId === 'model_set_default_btn') {
448
532
  await interaction.deferUpdate();
449
- const cdp = deps.getCurrentCdp(deps.bridge);
533
+ const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
450
534
  if (!cdp) {
451
535
  await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
452
536
  return;
453
537
  }
538
+ const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
539
+ if (!sessionReady.ok) {
540
+ await interaction.followUp({ content: sessionReady.error, flags: discord_js_1.MessageFlags.Ephemeral });
541
+ return;
542
+ }
454
543
  const currentModel = await cdp.getCurrentModel();
455
544
  if (!currentModel) {
456
545
  await interaction.followUp({ content: 'No current model detected.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -461,7 +550,7 @@ function createInteractionCreateHandler(deps) {
461
550
  deps.userPrefRepo.setDefaultModel(interaction.user.id, currentModel);
462
551
  }
463
552
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
464
- getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
553
+ getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
465
554
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
466
555
  });
467
556
  await interaction.followUp({ content: `Default model set to **${currentModel}**.`, flags: discord_js_1.MessageFlags.Ephemeral });
@@ -469,12 +558,20 @@ function createInteractionCreateHandler(deps) {
469
558
  }
470
559
  if (interaction.customId === 'model_clear_default_btn') {
471
560
  await interaction.deferUpdate();
561
+ const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
562
+ if (cdp) {
563
+ const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
564
+ if (!sessionReady.ok) {
565
+ await interaction.followUp({ content: sessionReady.error, flags: discord_js_1.MessageFlags.Ephemeral });
566
+ return;
567
+ }
568
+ }
472
569
  deps.modelService.setDefaultModel(null);
473
570
  if (deps.userPrefRepo) {
474
571
  deps.userPrefRepo.setDefaultModel(interaction.user.id, null);
475
572
  }
476
573
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
477
- getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
574
+ getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
478
575
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
479
576
  });
480
577
  await interaction.followUp({ content: 'Default model cleared.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -482,8 +579,16 @@ function createInteractionCreateHandler(deps) {
482
579
  }
483
580
  if (interaction.customId === 'model_refresh_btn') {
484
581
  await interaction.deferUpdate();
582
+ const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
583
+ if (cdp) {
584
+ const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
585
+ if (!sessionReady.ok) {
586
+ await interaction.followUp({ content: sessionReady.error, flags: discord_js_1.MessageFlags.Ephemeral });
587
+ return;
588
+ }
589
+ }
485
590
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
486
- getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
591
+ getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
487
592
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
488
593
  });
489
594
  return;
@@ -491,18 +596,23 @@ function createInteractionCreateHandler(deps) {
491
596
  if (interaction.customId.startsWith('model_btn_')) {
492
597
  await interaction.deferUpdate();
493
598
  const modelName = interaction.customId.replace('model_btn_', '');
494
- const cdp = deps.getCurrentCdp(deps.bridge);
599
+ const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
495
600
  if (!cdp) {
496
601
  await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
497
602
  return;
498
603
  }
604
+ const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
605
+ if (!sessionReady.ok) {
606
+ await interaction.followUp({ content: sessionReady.error, flags: discord_js_1.MessageFlags.Ephemeral });
607
+ return;
608
+ }
499
609
  const res = await cdp.setUiModel(modelName);
500
610
  if (!res.ok) {
501
611
  await interaction.followUp({ content: res.error || 'Failed to change model.', flags: discord_js_1.MessageFlags.Ephemeral });
502
612
  }
503
613
  else {
504
614
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
505
- getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
615
+ getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
506
616
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
507
617
  });
508
618
  await interaction.followUp({ content: `Model changed to **${res.model}**!`, flags: discord_js_1.MessageFlags.Ephemeral });
@@ -589,7 +699,7 @@ function createInteractionCreateHandler(deps) {
589
699
  try {
590
700
  const selectedMode = interaction.values[0];
591
701
  deps.modeService.setMode(selectedMode);
592
- const cdp = deps.getCurrentCdp(deps.bridge);
702
+ const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
593
703
  if (cdp) {
594
704
  const res = await cdp.setUiMode(selectedMode);
595
705
  if (!res.ok) {
@@ -612,6 +722,75 @@ function createInteractionCreateHandler(deps) {
612
722
  }
613
723
  return;
614
724
  }
725
+ if (interaction.isStringSelectMenu() && interaction.customId === accountUi_1.ACCOUNT_SELECT_ID) {
726
+ if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
727
+ await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
728
+ return;
729
+ }
730
+ try {
731
+ await interaction.deferUpdate();
732
+ }
733
+ catch (deferError) {
734
+ if (deferError?.code === 10062 || deferError?.code === 40060) {
735
+ logger_1.logger.warn('[Account] deferUpdate expired. Skipping.');
736
+ return;
737
+ }
738
+ logger_1.logger.error('[Account] deferUpdate failed:', deferError);
739
+ return;
740
+ }
741
+ try {
742
+ if (!deps.accountPrefRepo) {
743
+ await interaction.followUp({
744
+ content: 'Account preference service not available.',
745
+ flags: discord_js_1.MessageFlags.Ephemeral,
746
+ }).catch(logger_1.logger.error);
747
+ return;
748
+ }
749
+ const selectedAccount = interaction.values[0];
750
+ const names = (0, accountUtils_1.listAccountNames)(deps.antigravityAccounts);
751
+ if (!selectedAccount || !names.includes(selectedAccount)) {
752
+ await interaction.followUp({
753
+ content: `⚠️ Unknown account: **${selectedAccount || 'N/A'}**`,
754
+ flags: discord_js_1.MessageFlags.Ephemeral,
755
+ }).catch(logger_1.logger.error);
756
+ return;
757
+ }
758
+ deps.bridge.selectedAccountByChannel?.set(interaction.channelId, selectedAccount);
759
+ const currentSession = deps.chatSessionRepo?.findByChannelId(interaction.channelId);
760
+ if (currentSession) {
761
+ deps.chatSessionRepo?.setActiveAccountName(interaction.channelId, selectedAccount);
762
+ }
763
+ else {
764
+ deps.accountPrefRepo.setAccountName(interaction.user.id, selectedAccount);
765
+ deps.channelPrefRepo?.setAccountName(interaction.channelId, selectedAccount);
766
+ }
767
+ const channelWorkspace = deps.wsHandler.getWorkspaceForChannel(interaction.channelId);
768
+ const selectedPort = deps.antigravityAccounts?.find((a) => a.name === selectedAccount)?.cdpPort;
769
+ logger_1.logger.info(`[AccountSwitch] source=select channel=${interaction.channelId} user=${interaction.user.id} ` +
770
+ `account=${selectedAccount} port=${selectedPort ?? 'unknown'} ` +
771
+ `workspace=${channelWorkspace ?? 'unbound'}`);
772
+ await (0, accountUi_1.sendAccountUI)({ editReply: async (data) => await interaction.editReply(data) }, selectedAccount, names);
773
+ await interaction.followUp({
774
+ content: `✅ Switched session account to **${selectedAccount}**.`,
775
+ flags: discord_js_1.MessageFlags.Ephemeral,
776
+ }).catch(logger_1.logger.error);
777
+ }
778
+ catch (error) {
779
+ logger_1.logger.error('Error during account dropdown handling:', error);
780
+ try {
781
+ if (interaction.deferred || interaction.replied) {
782
+ await interaction.followUp({
783
+ content: 'An error occurred while switching account.',
784
+ flags: discord_js_1.MessageFlags.Ephemeral,
785
+ }).catch(logger_1.logger.error);
786
+ }
787
+ }
788
+ catch (e) {
789
+ logger_1.logger.error('Failed to send error message:', e);
790
+ }
791
+ }
792
+ return;
793
+ }
615
794
  if (interaction.isStringSelectMenu() && (0, sessionPickerUi_1.isSessionSelectId)(interaction.customId)) {
616
795
  if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
617
796
  await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
@@ -648,6 +827,7 @@ function createInteractionCreateHandler(deps) {
648
827
  return;
649
828
  }
650
829
  try {
830
+ await interaction.deferUpdate();
651
831
  await deps.wsHandler.handleSelectMenu(interaction, interaction.guild);
652
832
  }
653
833
  catch (error) {
@@ -681,10 +861,11 @@ function createInteractionCreateHandler(deps) {
681
861
  throw deferError;
682
862
  }
683
863
  try {
684
- await deps.handleSlashInteraction(commandInteraction, deps.slashCommandHandler, deps.bridge, deps.wsHandler, deps.chatHandler, deps.cleanupHandler, deps.modeService, deps.modelService, deps.bridge.autoAccept, deps.client);
864
+ await deps.handleSlashInteraction(commandInteraction, deps.slashCommandHandler, deps.bridge, deps.wsHandler, deps.chatHandler, deps.cleanupHandler, deps.modeService, deps.modelService, deps.bridge.autoAccept, deps.client, deps.accountPrefRepo, deps.channelPrefRepo, deps.antigravityAccounts, deps.chatSessionRepo);
685
865
  }
686
866
  catch (error) {
687
- logger_1.logger.error('Error during slash command handling:', error);
867
+ logger_1.logger.error(`[SlashCommand] command=${commandInteraction.commandName} channel=${commandInteraction.channelId} ` +
868
+ `user=${commandInteraction.user.id} failed:`, error);
688
869
  try {
689
870
  await commandInteraction.editReply({ content: 'An error occurred while processing the command.' });
690
871
  }
@@ -8,6 +8,7 @@ const wrappers_1 = require("../platform/discord/wrappers");
8
8
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
9
9
  const modeService_1 = require("../services/modeService");
10
10
  const imageHandler_1 = require("../utils/imageHandler");
11
+ const accountUtils_1 = require("../utils/accountUtils");
11
12
  const logger_1 = require("../utils/logger");
12
13
  function createMessageCreateHandler(deps) {
13
14
  const getCurrentCdp = deps.getCurrentCdp ?? cdpBridgeManager_1.getCurrentCdp;
@@ -20,6 +21,15 @@ function createMessageCreateHandler(deps) {
20
21
  const downloadInboundImageAttachments = deps.downloadInboundImageAttachments ?? imageHandler_1.downloadInboundImageAttachments;
21
22
  const cleanupInboundImageAttachments = deps.cleanupInboundImageAttachments ?? imageHandler_1.cleanupInboundImageAttachments;
22
23
  const isImageAttachment = deps.isImageAttachment ?? imageHandler_1.isImageAttachment;
24
+ const getParentChannelId = (message) => {
25
+ const parentId = message.channel?.parentId;
26
+ return typeof parentId === 'string' && parentId.length > 0 ? parentId : null;
27
+ };
28
+ const getAccountPort = (accountName) => {
29
+ const match = (deps.antigravityAccounts ?? []).find((account) => account.name === accountName);
30
+ return match ? match.cdpPort : null;
31
+ };
32
+ const getSessionAccountName = (channelId) => deps.chatSessionRepo.findByChannelId(channelId)?.activeAccountName ?? null;
23
33
  // Per-workspace prompt queue: serializes send→response cycles
24
34
  const workspaceQueues = new Map();
25
35
  const workspaceQueueDepths = new Map();
@@ -58,10 +68,25 @@ function createMessageCreateHandler(deps) {
58
68
  if (parsed.commandName === 'status') {
59
69
  const activeNames = deps.bridge.pool.getActiveWorkspaceNames();
60
70
  const currentMode = deps.modeService.getCurrentMode();
71
+ const session = deps.chatSessionRepo.findByChannelId(message.channelId);
72
+ const currentAccount = (0, accountUtils_1.resolveScopedAccountName)({
73
+ channelId: message.channelId,
74
+ userId: message.author.id,
75
+ sessionAccountName: getSessionAccountName(message.channelId),
76
+ parentChannelId: getParentChannelId(message),
77
+ selectedAccountByChannel: deps.bridge.selectedAccountByChannel,
78
+ channelPrefRepo: deps.channelPrefRepo,
79
+ accountPrefRepo: deps.accountPrefRepo,
80
+ accounts: deps.antigravityAccounts,
81
+ });
82
+ const conversationTitle = session?.displayName ?? '(New chat / no saved title)';
61
83
  const statusFields = [
62
84
  { name: 'CDP Connection', value: activeNames.length > 0 ? `🟢 ${activeNames.length} project(s) connected` : '⚪ Disconnected', inline: true },
63
85
  { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true },
64
86
  { name: 'Auto Approve', value: deps.bridge.autoAccept.isEnabled() ? '🟢 ON' : '⚪ OFF', inline: true },
87
+ { name: 'Active Account', value: currentAccount, inline: true },
88
+ { name: 'Original Account', value: session?.originAccountName ?? '(unset)', inline: true },
89
+ { name: 'Conversation Title', value: conversationTitle, inline: false },
65
90
  ];
66
91
  let statusDescription = '';
67
92
  if (activeNames.length > 0) {
@@ -97,6 +122,43 @@ function createMessageCreateHandler(deps) {
97
122
  await message.reply({ embeds: [embed] });
98
123
  return;
99
124
  }
125
+ if (parsed.commandName === 'account') {
126
+ const accountNames = (0, accountUtils_1.listAccountNames)(deps.antigravityAccounts);
127
+ const requested = parsed.args?.[0];
128
+ if (!requested) {
129
+ const current = (0, accountUtils_1.resolveScopedAccountName)({
130
+ channelId: message.channelId,
131
+ userId: message.author.id,
132
+ sessionAccountName: getSessionAccountName(message.channelId),
133
+ parentChannelId: getParentChannelId(message),
134
+ selectedAccountByChannel: deps.bridge.selectedAccountByChannel,
135
+ channelPrefRepo: deps.channelPrefRepo,
136
+ accountPrefRepo: deps.accountPrefRepo,
137
+ accounts: deps.antigravityAccounts,
138
+ });
139
+ await message.reply(`Current account: **${current}**\nAvailable: ${accountNames.join(', ')}`).catch(() => { });
140
+ return;
141
+ }
142
+ if (!accountNames.includes(requested)) {
143
+ await message.reply(`⚠️ Unknown account: **${requested}**`).catch(() => { });
144
+ return;
145
+ }
146
+ deps.bridge.selectedAccountByChannel?.set(message.channelId, requested);
147
+ const currentSession = deps.chatSessionRepo.findByChannelId(message.channelId);
148
+ if (currentSession) {
149
+ deps.chatSessionRepo.setActiveAccountName(message.channelId, requested);
150
+ }
151
+ else {
152
+ deps.accountPrefRepo?.setAccountName(message.author.id, requested);
153
+ deps.channelPrefRepo?.setAccountName(message.channelId, requested);
154
+ }
155
+ const channelWorkspace = deps.wsHandler.getWorkspaceForChannel(message.channelId);
156
+ logger_1.logger.info(`[AccountSwitch] source=text channel=${message.channelId} user=${message.author.id} ` +
157
+ `account=${requested} port=${getAccountPort(requested) ?? 'unknown'} ` +
158
+ `workspace=${channelWorkspace ?? 'unbound'}`);
159
+ await message.reply(`✅ Switched session account to **${requested}**.`).catch(() => { });
160
+ return;
161
+ }
100
162
  const slashOnlyCommands = ['help', 'stop', 'model', 'mode', 'project', 'chat', 'new', 'cleanup', 'join', 'mirror', 'output'];
101
163
  if (slashOnlyCommands.includes(parsed.commandName)) {
102
164
  await message.reply({
@@ -163,17 +225,48 @@ function createMessageCreateHandler(deps) {
163
225
  await message.reactions.resolve('⏳')?.users.remove(botId).catch(() => { });
164
226
  }
165
227
  try {
166
- const cdp = await deps.bridge.pool.getOrConnect(workspacePath);
228
+ const selectedAccount = (0, accountUtils_1.resolveScopedAccountName)({
229
+ channelId: message.channelId,
230
+ userId: message.author.id,
231
+ sessionAccountName: getSessionAccountName(message.channelId),
232
+ parentChannelId: getParentChannelId(message),
233
+ selectedAccountByChannel: deps.bridge.selectedAccountByChannel,
234
+ channelPrefRepo: deps.channelPrefRepo,
235
+ accountPrefRepo: deps.accountPrefRepo,
236
+ accounts: deps.antigravityAccounts,
237
+ });
238
+ const selectedPort = getAccountPort(selectedAccount);
239
+ deps.bridge.selectedAccountByChannel?.set(message.channelId, selectedAccount);
240
+ logger_1.logger.info(`[Route] channel=${message.channelId} user=${message.author.id} ` +
241
+ `project=${projectLabel} account=${selectedAccount} ` +
242
+ `port=${selectedPort ?? 'unknown'} workspacePath=${workspacePath}`);
243
+ const previousPreferredAccount = deps.bridge.pool.getPreferredAccountForWorkspace?.(workspacePath) ?? null;
244
+ const cdp = await deps.bridge.pool.getOrConnect(workspacePath, { name: selectedAccount });
167
245
  const projectName = deps.bridge.pool.extractProjectName(workspacePath);
246
+ deps.bridge.pool.setPreferredAccountForWorkspace?.(workspacePath, selectedAccount);
168
247
  deps.bridge.lastActiveWorkspace = projectName;
169
248
  const platformChannel = (0, wrappers_1.wrapDiscordChannel)(message.channel);
170
249
  deps.bridge.lastActiveChannel = platformChannel;
171
250
  registerApprovalWorkspaceChannel(deps.bridge, projectName, platformChannel);
172
- ensureApprovalDetector(deps.bridge, cdp, projectName);
173
- ensureErrorPopupDetector(deps.bridge, cdp, projectName);
174
- ensurePlanningDetector(deps.bridge, cdp, projectName);
175
- ensureRunCommandDetector(deps.bridge, cdp, projectName);
176
- const session = deps.chatSessionRepo.findByChannelId(message.channelId);
251
+ ensureApprovalDetector(deps.bridge, cdp, projectName, selectedAccount);
252
+ ensureErrorPopupDetector(deps.bridge, cdp, projectName, selectedAccount);
253
+ ensurePlanningDetector(deps.bridge, cdp, projectName, selectedAccount);
254
+ ensureRunCommandDetector(deps.bridge, cdp, projectName, selectedAccount);
255
+ let session = deps.chatSessionRepo.findByChannelId(message.channelId);
256
+ const staleSessionAccount = session?.isRenamed
257
+ && ((session.activeAccountName && session.activeAccountName !== selectedAccount)
258
+ || (!session.activeAccountName && previousPreferredAccount && previousPreferredAccount !== selectedAccount));
259
+ if (session && staleSessionAccount) {
260
+ logger_1.logger.info(`[SessionAccountReset] channel=${message.channelId} ` +
261
+ `project=${projectName} oldAccount=${session.activeAccountName ?? previousPreferredAccount ?? 'unknown'} ` +
262
+ `newAccount=${selectedAccount}`);
263
+ deps.chatSessionRepo.setActiveAccountName?.(message.channelId, selectedAccount);
264
+ session = deps.chatSessionRepo.findByChannelId(message.channelId);
265
+ }
266
+ if (session) {
267
+ deps.chatSessionRepo.setActiveAccountName?.(message.channelId, selectedAccount);
268
+ deps.chatSessionRepo.initializeOriginAccountName?.(message.channelId, selectedAccount);
269
+ }
177
270
  if (session?.displayName) {
178
271
  registerApprovalSessionChannel(deps.bridge, projectName, session.displayName, platformChannel);
179
272
  }
@@ -198,6 +291,8 @@ function createMessageCreateHandler(deps) {
198
291
  `"${session.displayName}" -> "${recoveredTitle}" ` +
199
292
  `(channel: ${message.channelId})`);
200
293
  deps.chatSessionRepo.updateDisplayName(message.channelId, recoveredTitle);
294
+ deps.chatSessionRepo.setActiveAccountName?.(message.channelId, selectedAccount);
295
+ deps.chatSessionRepo.initializeOriginAccountName?.(message.channelId, selectedAccount);
201
296
  registerApprovalSessionChannel(deps.bridge, projectName, recoveredTitle, platformChannel);
202
297
  }
203
298
  activationResult = retryResult;
@@ -290,7 +385,8 @@ function createMessageCreateHandler(deps) {
290
385
  });
291
386
  }
292
387
  else {
293
- await message.reply('No project is configured for this channel. Please create or select one with `/project`.');
388
+ await message.reply('No project is configured for this channel. Use `/project` to bind one, ' +
389
+ 'or `/project reopen` if this is a previously used session.');
294
390
  }
295
391
  }
296
392
  finally {
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAccountSelectAction = createAccountSelectAction;
4
+ const accountUtils_1 = require("../utils/accountUtils");
5
+ const accountUi_1 = require("../ui/accountUi");
6
+ const logger_1 = require("../utils/logger");
7
+ function createAccountSelectAction(deps) {
8
+ return {
9
+ match(customId) {
10
+ return customId === accountUi_1.ACCOUNT_SELECT_ID;
11
+ },
12
+ async execute(interaction, values) {
13
+ const selectedAccount = values[0];
14
+ if (!selectedAccount)
15
+ return;
16
+ const names = (0, accountUtils_1.listAccountNames)(deps.antigravityAccounts);
17
+ if (!names.includes(selectedAccount)) {
18
+ await interaction.followUp({
19
+ text: `⚠️ Unknown account: **${selectedAccount}**`,
20
+ }).catch(() => { });
21
+ return;
22
+ }
23
+ await interaction.deferUpdate();
24
+ deps.bridge.selectedAccountByChannel?.set(interaction.channel.id, selectedAccount);
25
+ const currentSession = deps.chatSessionRepo?.findByChannelId(interaction.channel.id);
26
+ if (currentSession) {
27
+ deps.chatSessionRepo?.setActiveAccountName(interaction.channel.id, selectedAccount);
28
+ }
29
+ else {
30
+ deps.accountPrefRepo.setAccountName(interaction.user.id, selectedAccount);
31
+ deps.channelPrefRepo?.setAccountName(interaction.channel.id, selectedAccount);
32
+ }
33
+ const channelWorkspace = deps.getWorkspacePathForChannel?.(interaction.channel.id) ?? null;
34
+ const selectedPort = deps.antigravityAccounts.find((a) => a.name === selectedAccount)?.cdpPort;
35
+ logger_1.logger.info(`[AccountSwitch] source=select channel=${interaction.channel.id} user=${interaction.user.id} ` +
36
+ `account=${selectedAccount} port=${selectedPort ?? 'unknown'} ` +
37
+ `workspace=${channelWorkspace ?? 'unbound'}`);
38
+ const payload = (0, accountUi_1.buildAccountPayload)(selectedAccount, names);
39
+ await interaction.update(payload);
40
+ await interaction.followUp({
41
+ text: `✅ Switched session account to **${selectedAccount}**.`,
42
+ }).catch(() => { });
43
+ },
44
+ };
45
+ }
@@ -32,9 +32,17 @@ function createModelButtonAction(deps) {
32
32
  await interaction.deferUpdate();
33
33
  const cdp = (0, cdpBridgeManager_1.getCurrentCdp)(deps.bridge);
34
34
  if (!cdp) {
35
+ logger_1.logger.warn(`[ModelCommand] source=button user=${interaction.user.id} action=${params.action} cdp=unavailable`);
35
36
  await interaction.followUp({ text: 'Not connected to CDP.' }).catch(() => { });
36
37
  return;
37
38
  }
39
+ if (deps.ensureSessionActivated) {
40
+ const sessionReady = await deps.ensureSessionActivated(interaction.channel.id, interaction.user.id, cdp);
41
+ if (!sessionReady.ok) {
42
+ await interaction.followUp({ text: sessionReady.error }).catch(() => { });
43
+ return;
44
+ }
45
+ }
38
46
  if (params.action === 'set_default') {
39
47
  const currentModel = await cdp.getCurrentModel();
40
48
  if (!currentModel) {
@@ -65,7 +73,12 @@ function createModelButtonAction(deps) {
65
73
  }).catch(() => { });
66
74
  }
67
75
  else if (params.action === 'select') {
76
+ logger_1.logger.info(`[ModelCommand] source=button user=${interaction.user.id} target="${params.modelName}"`);
68
77
  const res = await cdp.setUiModel(params.modelName);
78
+ logger_1.logger.info(`[ModelCommand] source=button user=${interaction.user.id} target="${params.modelName}" ` +
79
+ `ok=${res.ok} applied=${res.model ? `"${res.model}"` : 'null'} ` +
80
+ `verified=${res.verified === true} alreadySelected=${res.alreadySelected === true} ` +
81
+ `error=${res.error ? `"${res.error}"` : 'null'}`);
69
82
  if (!res.ok) {
70
83
  await interaction.followUp({
71
84
  text: res.error || 'Failed to change model.',