lazy-gravity 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/bot/index.js +233 -41
  2. package/dist/bot/telegramJoinCommand.js +9 -0
  3. package/dist/bot/telegramMessageHandler.js +7 -0
  4. package/dist/commands/chatCommandHandler.js +71 -1
  5. package/dist/commands/registerSlashCommands.js +13 -1
  6. package/dist/events/interactionCreateHandler.js +324 -26
  7. package/dist/events/messageCreateHandler.js +53 -4
  8. package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
  9. package/dist/handlers/approvalButtonAction.js +2 -1
  10. package/dist/handlers/errorPopupButtonAction.js +2 -1
  11. package/dist/handlers/fileChangeButtonAction.js +69 -0
  12. package/dist/handlers/genericActionButtonAction.js +74 -0
  13. package/dist/handlers/planningButtonAction.js +24 -9
  14. package/dist/handlers/questionSelectAction.js +69 -0
  15. package/dist/handlers/questionSkipAction.js +67 -0
  16. package/dist/handlers/runCommandButtonAction.js +2 -1
  17. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  18. package/dist/services/approvalDetector.js +70 -24
  19. package/dist/services/artifactService.js +57 -31
  20. package/dist/services/assistantDomExtractor.js +174 -3
  21. package/dist/services/cdpBridgeManager.js +114 -6
  22. package/dist/services/cdpConnectionPool.js +15 -0
  23. package/dist/services/cdpService.js +21 -7
  24. package/dist/services/chatSessionService.js +148 -27
  25. package/dist/services/errorPopupDetector.js +23 -11
  26. package/dist/services/notificationSender.js +70 -3
  27. package/dist/services/planningDetector.js +69 -25
  28. package/dist/services/promptDispatcher.js +7 -1
  29. package/dist/services/questionDetector.js +376 -0
  30. package/dist/services/responseMonitor.js +28 -2
  31. package/dist/services/runCommandDetector.js +14 -7
  32. package/dist/ui/artifactsUi.js +4 -3
  33. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  34. package/dist/utils/fileOpenCache.js +33 -0
  35. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  36. package/dist/utils/pathUtils.js +12 -0
  37. package/dist/utils/projectResolver.js +10 -0
  38. package/dist/utils/questionActionUtils.js +25 -0
  39. package/package.json +2 -2
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fileChangeButtonAction_1 = require("../fileChangeButtonAction");
4
+ describe('fileChangeButtonAction', () => {
5
+ let mockInteraction;
6
+ let mockCdp;
7
+ let mockBridge;
8
+ let mockWsHandler;
9
+ beforeEach(() => {
10
+ mockInteraction = {
11
+ deferUpdate: jest.fn().mockResolvedValue(undefined),
12
+ reply: jest.fn().mockResolvedValue(undefined),
13
+ update: jest.fn().mockResolvedValue(undefined),
14
+ followUp: jest.fn().mockResolvedValue(undefined),
15
+ };
16
+ mockCdp = {
17
+ isConnected: jest.fn().mockReturnValue(true),
18
+ call: jest.fn(),
19
+ };
20
+ mockBridge = {
21
+ lastActiveWorkspace: 'test_project',
22
+ pool: {
23
+ getConnected: jest.fn().mockReturnValue(mockCdp),
24
+ extractProjectName: jest.fn().mockReturnValue('test_project'),
25
+ },
26
+ };
27
+ mockWsHandler = {
28
+ getWorkspaceForChannel: jest.fn().mockReturnValue('test_project'),
29
+ };
30
+ });
31
+ it('matches valid legacy custom IDs', () => {
32
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
33
+ expect(action.match('ide_file_accept_all')).toEqual({ action: 'accept', projectName: '', channelId: '' });
34
+ expect(action.match('ide_file_reject_all')).toEqual({ action: 'reject', projectName: '', channelId: '' });
35
+ expect(action.match('unknown_button')).toBeNull();
36
+ });
37
+ it('matches valid custom IDs with project and channel', () => {
38
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
39
+ expect(action.match('file_change_accept:proj:chan1')).toEqual({ action: 'accept', projectName: 'proj', channelId: 'chan1' });
40
+ expect(action.match('file_change_reject:proj:chan2')).toEqual({ action: 'reject', projectName: 'proj', channelId: 'chan2' });
41
+ });
42
+ it('fails if linked to a different session channel', async () => {
43
+ mockInteraction.channel = { id: 'channel_123' };
44
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
45
+ await action.execute(mockInteraction, { action: 'accept', channelId: 'channel_456' });
46
+ expect(mockInteraction.reply).toHaveBeenCalledWith({ text: 'This file change action is linked to a different session channel.' });
47
+ });
48
+ it('fails if not connected to IDE', async () => {
49
+ mockInteraction.channel = { id: 'channel_123' };
50
+ mockCdp.isConnected.mockReturnValue(false);
51
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
52
+ await action.execute(mockInteraction, { action: 'accept', channelId: 'channel_123' });
53
+ expect(mockInteraction.reply).toHaveBeenCalledWith({ text: 'Not connected to IDE.' });
54
+ });
55
+ it('successfully accepts changes when button is found', async () => {
56
+ mockInteraction.channel = { id: 'channel_123' };
57
+ mockCdp.call.mockResolvedValue({ result: { value: true } });
58
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
59
+ await action.execute(mockInteraction, { action: 'accept' });
60
+ expect(mockCdp.call).toHaveBeenCalled();
61
+ expect(mockInteraction.update).toHaveBeenCalledWith({
62
+ text: '✅ Accepted file changes.',
63
+ components: [],
64
+ });
65
+ });
66
+ it('successfully rejects changes when button is found', async () => {
67
+ mockInteraction.channel = { id: 'channel_123' };
68
+ mockCdp.call.mockResolvedValue({ result: { value: true } });
69
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
70
+ await action.execute(mockInteraction, { action: 'reject' });
71
+ expect(mockInteraction.update).toHaveBeenCalledWith({
72
+ text: '❌ Rejected file changes.',
73
+ components: [],
74
+ });
75
+ });
76
+ it('sends followup if button is not found', async () => {
77
+ mockInteraction.channel = { id: 'channel_123' };
78
+ mockCdp.call.mockResolvedValue({ result: { value: false } });
79
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
80
+ await action.execute(mockInteraction, { action: 'accept' });
81
+ expect(mockInteraction.followUp).toHaveBeenCalledWith({
82
+ text: expect.stringContaining('Could not find the "Accept all" button'),
83
+ });
84
+ });
85
+ });
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createApprovalButtonAction = createApprovalButtonAction;
10
10
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
+ const projectResolver_1 = require("../utils/projectResolver");
11
12
  const logger_1 = require("../utils/logger");
12
13
  function createApprovalButtonAction(deps) {
13
14
  return {
@@ -32,7 +33,7 @@ function createApprovalButtonAction(deps) {
32
33
  .catch(() => { });
33
34
  return;
34
35
  }
35
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
36
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
36
37
  logger_1.logger.debug(`[ApprovalAction] action=${action} project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
37
38
  const detector = projectName
38
39
  ? deps.bridge.pool.getApprovalDetector(projectName)
@@ -7,6 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createErrorPopupButtonAction = createErrorPopupButtonAction;
10
+ const projectResolver_1 = require("../utils/projectResolver");
10
11
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
12
  const logger_1 = require("../utils/logger");
12
13
  const MAX_DEBUG_CONTENT = 4096;
@@ -30,7 +31,7 @@ function createErrorPopupButtonAction(deps) {
30
31
  .catch(() => { });
31
32
  return;
32
33
  }
33
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
34
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
34
35
  const detector = projectName
35
36
  ? deps.bridge.pool.getErrorPopupDetector(projectName)
36
37
  : undefined;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFileChangeButtonAction = createFileChangeButtonAction;
4
+ const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
5
+ const projectResolver_1 = require("../utils/projectResolver");
6
+ const logger_1 = require("../utils/logger");
7
+ const approvalDetector_1 = require("../services/approvalDetector");
8
+ function createFileChangeButtonAction(deps) {
9
+ return {
10
+ match(customId) {
11
+ const parsed = (0, cdpBridgeManager_1.parseFileChangeCustomId)(customId);
12
+ if (!parsed)
13
+ return null;
14
+ return {
15
+ action: parsed.action,
16
+ projectName: parsed.projectName ?? '',
17
+ channelId: parsed.channelId ?? '',
18
+ };
19
+ },
20
+ async execute(interaction, params) {
21
+ const { action, channelId } = params;
22
+ // Acknowledge immediately so Telegram/Discord doesn't time out
23
+ await interaction.deferUpdate().catch(() => { });
24
+ if (channelId && channelId !== interaction.channel.id) {
25
+ await interaction
26
+ .reply({ text: 'This file change action is linked to a different session channel.' })
27
+ .catch(() => { });
28
+ return;
29
+ }
30
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
31
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName) : undefined;
32
+ if (!cdp || !cdp.isConnected()) {
33
+ await interaction
34
+ .reply({ text: 'Not connected to IDE.' })
35
+ .catch(() => { });
36
+ return;
37
+ }
38
+ const targetText = action === 'accept' ? 'Accept all' : 'Reject all';
39
+ try {
40
+ const result = await cdp.call('Runtime.evaluate', {
41
+ expression: (0, approvalDetector_1.buildClickScript)(targetText),
42
+ returnByValue: true,
43
+ awaitPromise: true,
44
+ });
45
+ if (result?.result?.value?.ok) {
46
+ await interaction
47
+ .update({
48
+ text: `${action === 'accept' ? '✅ Accepted' : '❌ Rejected'} file changes.`,
49
+ components: [],
50
+ })
51
+ .catch((err) => {
52
+ logger_1.logger.warn('[FileChangeAction] update failed:', err);
53
+ });
54
+ }
55
+ else {
56
+ await interaction
57
+ .followUp({ text: `Could not find the "${targetText}" button in the IDE. The file change prompt may have been dismissed.` })
58
+ .catch(() => { });
59
+ }
60
+ }
61
+ catch (error) {
62
+ logger_1.logger.error('[FileChangeAction] Error clicking button:', error);
63
+ await interaction
64
+ .followUp({ text: 'An error occurred while interacting with the IDE.' })
65
+ .catch(() => { });
66
+ }
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseGenericActionCustomId = parseGenericActionCustomId;
4
+ exports.createGenericActionButtonAction = createGenericActionButtonAction;
5
+ const logger_1 = require("../utils/logger");
6
+ const projectResolver_1 = require("../utils/projectResolver");
7
+ const approvalDetector_1 = require("../services/approvalDetector");
8
+ /**
9
+ * Parses a generic action button customId into its components.
10
+ * Format: action_btn_[action_name]:[project_name]:[channel_id]
11
+ *
12
+ * @param customId The customId to parse
13
+ * @returns Parsed action details, or null if not an action button
14
+ */
15
+ function parseGenericActionCustomId(customId) {
16
+ if (!customId.startsWith('action_btn_'))
17
+ return null;
18
+ const parts = customId.split(':');
19
+ const actionName = parts[0].replace('action_btn_', '').replace(/_/g, ' ');
20
+ // Capitalize first letter
21
+ const formattedName = actionName.charAt(0).toUpperCase() + actionName.slice(1);
22
+ return {
23
+ actionName: formattedName,
24
+ projectName: parts[1] || undefined,
25
+ channelId: parts[2] || undefined,
26
+ };
27
+ }
28
+ function createGenericActionButtonAction(deps) {
29
+ return {
30
+ match(customId) {
31
+ const parsed = parseGenericActionCustomId(customId);
32
+ if (!parsed)
33
+ return null;
34
+ const result = { actionName: parsed.actionName };
35
+ if (parsed.projectName)
36
+ result.projectName = parsed.projectName;
37
+ if (parsed.channelId)
38
+ result.channelId = parsed.channelId;
39
+ return result;
40
+ },
41
+ async execute(interaction, params) {
42
+ const actionName = params.actionName;
43
+ const channelId = params.channelId || interaction.channel?.id;
44
+ if (!channelId) {
45
+ return;
46
+ }
47
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, channelId, params.projectName);
48
+ await interaction.deferUpdate().catch(() => { });
49
+ const cdp = projectName
50
+ ? deps.bridge.pool.getConnected(projectName)
51
+ : null;
52
+ if (!cdp) {
53
+ await interaction.followUp({
54
+ text: 'Not connected to Antigravity. Send the action as a message instead.',
55
+ }).catch(() => { });
56
+ return;
57
+ }
58
+ // Simulate DOM click instead of chat injection
59
+ logger_1.logger.info(`[GenericActionButton] Clicking action button "${actionName}" via DOM script`);
60
+ const script = (0, approvalDetector_1.buildClickScript)(actionName);
61
+ const evalResult = await cdp.call('Runtime.evaluate', {
62
+ expression: script,
63
+ returnByValue: true,
64
+ });
65
+ const resultValue = evalResult.result?.value;
66
+ if (!resultValue?.ok) {
67
+ await interaction.followUp({
68
+ text: `Failed to execute action: Button "${actionName}" not found or obscured.`,
69
+ }).catch(() => { });
70
+ return;
71
+ }
72
+ },
73
+ };
74
+ }
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createPlanningButtonAction = createPlanningButtonAction;
10
10
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
+ const projectResolver_1 = require("../utils/projectResolver");
11
12
  const logger_1 = require("../utils/logger");
12
13
  const MAX_PLAN_CONTENT = 4096;
13
14
  function createPlanningButtonAction(deps) {
@@ -32,7 +33,7 @@ function createPlanningButtonAction(deps) {
32
33
  .catch(() => { });
33
34
  return;
34
35
  }
35
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
36
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
36
37
  const detector = projectName
37
38
  ? deps.bridge.pool.getPlanningDetector(projectName)
38
39
  : undefined;
@@ -43,15 +44,18 @@ function createPlanningButtonAction(deps) {
43
44
  return;
44
45
  }
45
46
  if (action === 'open') {
46
- const clicked = await detector.clickOpenButton();
47
- if (!clicked) {
48
- await interaction
49
- .reply({ text: 'Open button not found.' })
50
- .catch(() => { });
51
- return;
47
+ let clicked = false;
48
+ if (detector.getLastDetectedInfo()?.hasOpenButton) {
49
+ try {
50
+ clicked = await detector.clickOpenButton();
51
+ if (clicked) {
52
+ await new Promise((resolve) => setTimeout(resolve, 500));
53
+ }
54
+ }
55
+ catch (e) {
56
+ logger_1.logger.error('[PlanningAction] clickOpenButton failed:', e);
57
+ }
52
58
  }
53
- // Wait for DOM to update after Open click
54
- await new Promise((resolve) => setTimeout(resolve, 500));
55
59
  // Extract plan content with retry
56
60
  let planContent = null;
57
61
  for (let attempt = 0; attempt < 3; attempt++) {
@@ -60,6 +64,12 @@ function createPlanningButtonAction(deps) {
60
64
  break;
61
65
  await new Promise((resolve) => setTimeout(resolve, 500));
62
66
  }
67
+ if (!planContent && !clicked) {
68
+ await interaction
69
+ .reply({ text: 'Plan content could not be extracted from the IDE.' })
70
+ .catch(() => { });
71
+ return;
72
+ }
63
73
  await interaction
64
74
  .update({
65
75
  text: '📋 Plan opened',
@@ -84,6 +94,11 @@ function createPlanningButtonAction(deps) {
84
94
  .catch(() => { });
85
95
  }
86
96
  }
97
+ else if (action === 'reject') {
98
+ // Reject action
99
+ await interaction.reply({ text: 'Rejection of the plan is not allowed.' }).catch(() => { });
100
+ return;
101
+ }
87
102
  else {
88
103
  // Proceed action
89
104
  await interaction.deferUpdate().catch(() => { });
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createQuestionSelectAction = createQuestionSelectAction;
4
+ const logger_1 = require("../utils/logger");
5
+ const notificationSender_1 = require("../services/notificationSender");
6
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
7
+ const projectResolver_1 = require("../utils/projectResolver");
8
+ function createQuestionSelectAction(deps) {
9
+ return {
10
+ match(customId) {
11
+ return customId.startsWith(notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
12
+ },
13
+ async execute(interaction, values) {
14
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
15
+ if (!parsed)
16
+ return;
17
+ const selectedOption = parseInt(values[0], 10);
18
+ if (isNaN(selectedOption))
19
+ return;
20
+ const channelId = parsed.channelId;
21
+ if (channelId && channelId !== interaction.channel.id) {
22
+ await interaction
23
+ .reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
24
+ .catch(() => { });
25
+ return;
26
+ }
27
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
28
+ logger_1.logger.debug(`[QuestionSelectAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
29
+ const detector = projectName
30
+ ? deps.bridge.pool.getQuestionDetector(projectName)
31
+ : undefined;
32
+ if (!detector) {
33
+ logger_1.logger.warn(`[QuestionSelectAction] No detector for project=${projectName}`);
34
+ await interaction
35
+ .reply({ text: 'Question detector not found.', ephemeral: true })
36
+ .catch(() => { });
37
+ return;
38
+ }
39
+ await interaction.deferUpdate().catch(() => { });
40
+ let success = false;
41
+ try {
42
+ success = await detector.submitOption(selectedOption);
43
+ }
44
+ catch (err) {
45
+ const msg = err instanceof Error ? err.message : String(err);
46
+ logger_1.logger.error(`[QuestionSelectAction] CDP click failed: ${msg}`);
47
+ await interaction
48
+ .followUp({ text: `Failed to answer question: ${msg}`, ephemeral: true })
49
+ .catch(() => { });
50
+ return;
51
+ }
52
+ if (success) {
53
+ const updatePayload = { text: `✅ Submitted option ${selectedOption + 1}`, components: [] };
54
+ try {
55
+ await interaction.editReply(updatePayload);
56
+ }
57
+ catch (editErr) {
58
+ logger_1.logger.warn('[QuestionSelectAction] editReply failed, sending followUp:', editErr);
59
+ await interaction.followUp({ text: `✅ Submitted option ${selectedOption + 1}` }).catch(() => { });
60
+ }
61
+ }
62
+ else {
63
+ await interaction
64
+ .followUp({ text: 'Failed to answer question (it may have been resolved already).', ephemeral: true })
65
+ .catch(() => { });
66
+ }
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createQuestionSkipAction = createQuestionSkipAction;
4
+ const logger_1 = require("../utils/logger");
5
+ const notificationSender_1 = require("../services/notificationSender");
6
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
7
+ const projectResolver_1 = require("../utils/projectResolver");
8
+ function createQuestionSkipAction(deps) {
9
+ return {
10
+ match(customId) {
11
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
12
+ return parsed ? { action: parsed.action } : null;
13
+ },
14
+ async execute(interaction, params) {
15
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
16
+ if (!parsed)
17
+ return;
18
+ const channelId = parsed.channelId;
19
+ if (channelId && channelId !== interaction.channel.id) {
20
+ await interaction
21
+ .reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
22
+ .catch(() => { });
23
+ return;
24
+ }
25
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
26
+ logger_1.logger.debug(`[QuestionSkipAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
27
+ const detector = projectName
28
+ ? deps.bridge.pool.getQuestionDetector(projectName)
29
+ : undefined;
30
+ if (!detector) {
31
+ logger_1.logger.warn(`[QuestionSkipAction] No detector for project=${projectName}`);
32
+ await interaction
33
+ .reply({ text: 'Question detector not found.', ephemeral: true })
34
+ .catch(() => { });
35
+ return;
36
+ }
37
+ await interaction.deferUpdate().catch(() => { });
38
+ let success = false;
39
+ try {
40
+ success = await detector.skipQuestion();
41
+ }
42
+ catch (err) {
43
+ const msg = err instanceof Error ? err.message : String(err);
44
+ logger_1.logger.error(`[QuestionSkipAction] CDP click failed: ${msg}`);
45
+ await interaction
46
+ .followUp({ text: `Failed to skip question: ${msg}`, ephemeral: true })
47
+ .catch(() => { });
48
+ return;
49
+ }
50
+ if (success) {
51
+ const updatePayload = { text: `✅ Skipped question`, components: [] };
52
+ try {
53
+ await interaction.editReply(updatePayload);
54
+ }
55
+ catch (editErr) {
56
+ logger_1.logger.warn('[QuestionSkipAction] editReply failed, sending followUp:', editErr);
57
+ await interaction.followUp({ text: `✅ Skipped question` }).catch(() => { });
58
+ }
59
+ }
60
+ else {
61
+ await interaction
62
+ .followUp({ text: 'Failed to skip question (it may have been resolved already).', ephemeral: true })
63
+ .catch(() => { });
64
+ }
65
+ },
66
+ };
67
+ }
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createRunCommandButtonAction = createRunCommandButtonAction;
10
10
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
+ const projectResolver_1 = require("../utils/projectResolver");
11
12
  const logger_1 = require("../utils/logger");
12
13
  function createRunCommandButtonAction(deps) {
13
14
  return {
@@ -31,7 +32,7 @@ function createRunCommandButtonAction(deps) {
31
32
  .catch(() => { });
32
33
  return;
33
34
  }
34
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
35
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
35
36
  const detector = projectName
36
37
  ? deps.bridge.pool.getRunCommandDetector(projectName)
37
38
  : undefined;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDiscordResponse = renderDiscordResponse;
4
+ const discord_js_1 = require("discord.js");
5
+ /**
6
+ * Splits a long string into chunks that fit within Discord's embed description limit (4096 chars).
7
+ * Tries to split on double newlines, then single newlines, then spaces, before hard splitting.
8
+ */
9
+ function chunkText(text, limit = 4000) {
10
+ if (!text)
11
+ return [];
12
+ if (text.length <= limit)
13
+ return [text];
14
+ const chunks = [];
15
+ let remaining = text;
16
+ while (remaining.length > 0) {
17
+ if (remaining.length <= limit) {
18
+ chunks.push(remaining);
19
+ break;
20
+ }
21
+ let slice = remaining.slice(0, limit);
22
+ let splitIndex = slice.lastIndexOf('\n\n');
23
+ if (splitIndex === -1 || splitIndex < limit * 0.5) {
24
+ splitIndex = slice.lastIndexOf('\n');
25
+ }
26
+ if (splitIndex === -1 || splitIndex < limit * 0.5) {
27
+ splitIndex = slice.lastIndexOf(' ');
28
+ }
29
+ if (splitIndex === -1 || splitIndex < limit * 0.5) {
30
+ splitIndex = limit; // Hard split
31
+ }
32
+ chunks.push(remaining.slice(0, splitIndex).trim());
33
+ remaining = remaining.slice(splitIndex).trim();
34
+ }
35
+ return chunks;
36
+ }
37
+ /**
38
+ * Renders a classified Antigravity response into a Discord Message payload.
39
+ *
40
+ * Handles chunking for large outputs and structured cards (plan, files, actions).
41
+ */
42
+ function renderDiscordResponse(result, options) {
43
+ const embeds = [];
44
+ const components = [];
45
+ // 1. Final Output Text
46
+ if (result.finalOutputText) {
47
+ const textChunks = chunkText(result.finalOutputText, 4000); // leave some buffer
48
+ for (let i = 0; i < textChunks.length && i < 8; i++) { // max 10 embeds total, leave room
49
+ const embed = new discord_js_1.EmbedBuilder()
50
+ .setColor(0x2b2d31) // Discord dark mode background-ish / neutral
51
+ .setDescription(textChunks[i]);
52
+ embeds.push(embed);
53
+ }
54
+ }
55
+ // 2. Plan Cards
56
+ if (result.planCards && result.planCards.length > 0) {
57
+ const planText = result.planCards.join('\n\n');
58
+ const chunks = chunkText(planText, 4000);
59
+ for (let i = 0; i < chunks.length && embeds.length < 10; i++) {
60
+ const embed = new discord_js_1.EmbedBuilder()
61
+ .setTitle(i === 0 ? 'Plan' : null)
62
+ .setColor(0x5865f2) // Blurple
63
+ .setDescription(chunks[i]);
64
+ embeds.push(embed);
65
+ }
66
+ }
67
+ // 3. File Changes
68
+ if (result.fileChanges && result.fileChanges.length > 0) {
69
+ const embed = new discord_js_1.EmbedBuilder()
70
+ .setTitle('File Changes')
71
+ .setColor(0x3ba55c); // Greenish
72
+ // Chunk into fields if necessary (max 25 fields, 1024 chars per field)
73
+ let currentFieldVal = '';
74
+ let fieldCount = 0;
75
+ let processedCount = 0;
76
+ let truncated = false;
77
+ for (const file of result.fileChanges) {
78
+ if (fieldCount >= 24) {
79
+ truncated = true;
80
+ break;
81
+ }
82
+ const line = `- \`${file.path}\` (${file.type})\n`;
83
+ if (line.length > 1000) {
84
+ if (currentFieldVal) {
85
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: currentFieldVal });
86
+ currentFieldVal = '';
87
+ fieldCount++;
88
+ }
89
+ const splitLine = chunkText(line, 1000);
90
+ for (const sl of splitLine) {
91
+ if (fieldCount >= 24) {
92
+ truncated = true;
93
+ break;
94
+ }
95
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: sl || '-' });
96
+ fieldCount++;
97
+ }
98
+ }
99
+ else if (currentFieldVal.length + line.length > 1000) {
100
+ if (currentFieldVal) {
101
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: currentFieldVal });
102
+ fieldCount++;
103
+ }
104
+ currentFieldVal = line;
105
+ }
106
+ else {
107
+ currentFieldVal += line;
108
+ }
109
+ if (!truncated) {
110
+ processedCount++;
111
+ }
112
+ }
113
+ if (currentFieldVal && fieldCount < 24) {
114
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: currentFieldVal });
115
+ fieldCount++;
116
+ }
117
+ if (processedCount < result.fileChanges.length) {
118
+ const remaining = result.fileChanges.length - processedCount;
119
+ embed.addFields({ name: '...', value: `*...and ${remaining} more*` });
120
+ }
121
+ embeds.push(embed);
122
+ }
123
+ // 4. Action Buttons
124
+ if (result.actionButtons && result.actionButtons.length > 0) {
125
+ const row = new discord_js_1.ActionRowBuilder();
126
+ for (let i = 0; i < result.actionButtons.length && i < 5; i++) { // Max 5 buttons per row
127
+ const btnText = result.actionButtons[i];
128
+ let customId = `action_btn_${btnText.toLowerCase().replace(/\s+/g, '_')}`;
129
+ if (options?.projectName && options?.channelId) {
130
+ const overhead = customId.length + 2 + options.channelId.length;
131
+ const safeProject = options.projectName.length > (100 - overhead)
132
+ ? options.projectName.substring(0, 100 - overhead)
133
+ : options.projectName;
134
+ customId = `${customId}:${safeProject}:${options.channelId}`;
135
+ }
136
+ const btn = new discord_js_1.ButtonBuilder()
137
+ .setCustomId(customId)
138
+ .setLabel(btnText)
139
+ .setStyle(btnText.toLowerCase() === 'proceed' || btnText.toLowerCase() === 'open'
140
+ ? discord_js_1.ButtonStyle.Primary
141
+ : discord_js_1.ButtonStyle.Secondary);
142
+ row.addComponents(btn);
143
+ }
144
+ components.push(row);
145
+ }
146
+ // 5. Fallback if absolutely empty
147
+ if (embeds.length === 0) {
148
+ if (result.activityLines && result.activityLines.length > 0) {
149
+ const embed = new discord_js_1.EmbedBuilder()
150
+ .setColor(0x2b2d31)
151
+ .setDescription('*(Processing...)*\n' + result.activityLines.slice(-3).join('\n').slice(0, 3900));
152
+ embeds.push(embed);
153
+ }
154
+ else {
155
+ return { content: '*(No output)*' };
156
+ }
157
+ }
158
+ // Discord allows max 10 embeds per message.
159
+ const finalEmbeds = embeds.slice(0, 10);
160
+ return {
161
+ embeds: finalEmbeds,
162
+ components: components.length > 0 ? components : undefined,
163
+ };
164
+ }