lazy-gravity 0.9.2 → 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 (38) hide show
  1. package/dist/bot/index.js +232 -41
  2. package/dist/bot/telegramMessageHandler.js +1 -0
  3. package/dist/commands/chatCommandHandler.js +71 -1
  4. package/dist/commands/registerSlashCommands.js +13 -1
  5. package/dist/events/interactionCreateHandler.js +324 -26
  6. package/dist/events/messageCreateHandler.js +53 -4
  7. package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
  8. package/dist/handlers/approvalButtonAction.js +2 -1
  9. package/dist/handlers/errorPopupButtonAction.js +2 -1
  10. package/dist/handlers/fileChangeButtonAction.js +69 -0
  11. package/dist/handlers/genericActionButtonAction.js +74 -0
  12. package/dist/handlers/planningButtonAction.js +24 -9
  13. package/dist/handlers/questionSelectAction.js +69 -0
  14. package/dist/handlers/questionSkipAction.js +67 -0
  15. package/dist/handlers/runCommandButtonAction.js +2 -1
  16. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  17. package/dist/services/approvalDetector.js +70 -24
  18. package/dist/services/artifactService.js +57 -31
  19. package/dist/services/assistantDomExtractor.js +174 -3
  20. package/dist/services/cdpBridgeManager.js +114 -6
  21. package/dist/services/cdpConnectionPool.js +15 -0
  22. package/dist/services/cdpService.js +21 -7
  23. package/dist/services/chatSessionService.js +133 -33
  24. package/dist/services/errorPopupDetector.js +23 -11
  25. package/dist/services/notificationSender.js +70 -3
  26. package/dist/services/planningDetector.js +69 -25
  27. package/dist/services/promptDispatcher.js +7 -1
  28. package/dist/services/questionDetector.js +376 -0
  29. package/dist/services/responseMonitor.js +28 -2
  30. package/dist/services/runCommandDetector.js +14 -7
  31. package/dist/ui/artifactsUi.js +4 -3
  32. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  33. package/dist/utils/fileOpenCache.js +33 -0
  34. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  35. package/dist/utils/pathUtils.js +12 -0
  36. package/dist/utils/projectResolver.js +10 -0
  37. package/dist/utils/questionActionUtils.js +25 -0
  38. package/package.json +1 -1
@@ -601,6 +601,8 @@ class ResponseMonitor {
601
601
  extractionMode;
602
602
  onProgress;
603
603
  onComplete;
604
+ onStructuredProgress;
605
+ onStructuredComplete;
604
606
  onTimeout;
605
607
  onPhaseChange;
606
608
  onProcessLog;
@@ -609,12 +611,15 @@ class ResponseMonitor {
609
611
  pollTimer = null;
610
612
  isRunning = false;
611
613
  lastText = null;
614
+ lastClassified = null;
612
615
  baselineText = null;
613
616
  generationStarted = false;
614
617
  currentPhase = 'waiting';
615
618
  stopGoneCount = 0;
616
619
  quotaDetected = false;
617
620
  seenProcessLogKeys = new Set();
621
+ currentCitedFiles = [];
622
+ currentFileChanges = [];
618
623
  structuredDiagLogged = false;
619
624
  lastContentContextId = null;
620
625
  // CDP disconnect handling (#48)
@@ -632,12 +637,17 @@ class ResponseMonitor {
632
637
  this.extractionMode = options.extractionMode ?? 'structured';
633
638
  this.onProgress = options.onProgress;
634
639
  this.onComplete = options.onComplete;
640
+ this.onStructuredProgress = options.onStructuredProgress;
641
+ this.onStructuredComplete = options.onStructuredComplete;
635
642
  this.onTimeout = options.onTimeout;
636
643
  this.onPhaseChange = options.onPhaseChange;
637
644
  this.onProcessLog = options.onProcessLog;
638
645
  this.initialBaselineText = options.initialBaselineText;
639
646
  this.initialSeenProcessLogKeys = options.initialSeenProcessLogKeys;
640
647
  }
648
+ getLastClassified() {
649
+ return this.lastClassified;
650
+ }
641
651
  /** Start monitoring */
642
652
  async start() {
643
653
  return this.initMonitoring(false);
@@ -979,6 +989,7 @@ class ResponseMonitor {
979
989
  if (classified.diagnostics.source === 'dom-structured') {
980
990
  currentText = classified.finalOutputText.trim() || null;
981
991
  structuredHandledLogs = true;
992
+ this.lastClassified = classified;
982
993
  if (!this.structuredDiagLogged) {
983
994
  this.structuredDiagLogged = true;
984
995
  logger_1.logger.debug('[ResponseMonitor] Structured extraction OK — segments:', classified.diagnostics.segmentCounts);
@@ -987,6 +998,8 @@ class ResponseMonitor {
987
998
  if (classified.activityLines.length > 0) {
988
999
  this.emitNewProcessLogs(classified.activityLines);
989
1000
  }
1001
+ this.currentCitedFiles = classified.citedFiles || [];
1002
+ this.currentFileChanges = classified.fileChangesTexts || [];
990
1003
  }
991
1004
  else if (!this.structuredDiagLogged) {
992
1005
  this.structuredDiagLogged = true;
@@ -1037,6 +1050,8 @@ class ResponseMonitor {
1037
1050
  await this.stop();
1038
1051
  try {
1039
1052
  await Promise.resolve(this.onComplete?.(''));
1053
+ if (this.lastClassified)
1054
+ await Promise.resolve(this.onStructuredComplete?.(this.lastClassified));
1040
1055
  }
1041
1056
  catch (error) {
1042
1057
  logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
@@ -1061,7 +1076,16 @@ class ResponseMonitor {
1061
1076
  this.generationStarted = true;
1062
1077
  }
1063
1078
  }
1064
- this.onProgress?.(effectiveText);
1079
+ this.onProgress?.(effectiveText || '');
1080
+ if (this.lastClassified)
1081
+ this.onStructuredProgress?.(this.lastClassified);
1082
+ }
1083
+ else if (this.lastClassified && this.extractionMode === 'structured') {
1084
+ // If text didn't change but structured data did (e.g. new file changes), emit structured progress anyway
1085
+ // To keep it simple, we just emit on structured mode if generation is active.
1086
+ if (isGenerating && this.generationStarted) {
1087
+ this.onStructuredProgress?.(this.lastClassified);
1088
+ }
1065
1089
  }
1066
1090
  // Completion: stop button gone N consecutive times
1067
1091
  if (!isGenerating && this.generationStarted) {
@@ -1071,7 +1095,9 @@ class ResponseMonitor {
1071
1095
  this.setPhase('complete', finalText);
1072
1096
  await this.stop();
1073
1097
  try {
1074
- await Promise.resolve(this.onComplete?.(finalText));
1098
+ await Promise.resolve(this.onComplete?.(finalText, this.currentCitedFiles, this.currentFileChanges));
1099
+ if (this.lastClassified)
1100
+ await Promise.resolve(this.onStructuredComplete?.(this.lastClassified));
1075
1101
  }
1076
1102
  catch (error) {
1077
1103
  logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RunCommandDetector = void 0;
4
+ const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
4
5
  const logger_1 = require("../utils/logger");
5
6
  const approvalDetector_1 = require("./approvalDetector");
6
7
  /**
@@ -25,7 +26,7 @@ const approvalDetector_1 = require("./approvalDetector");
25
26
  */
26
27
  const DETECT_RUN_COMMAND_SCRIPT = `(() => {
27
28
  const RUN_COMMAND_HEADER_PATTERNS = [
28
- 'run command?', 'run command', 'execute command',
29
+ 'run command?', 'run command', 'execute command', 'command execution',
29
30
  'コマンドを実行', 'コマンド実行'
30
31
  ];
31
32
  const RUN_PATTERNS = ['run', 'accept', '実行', 'execute'];
@@ -111,8 +112,10 @@ class RunCommandDetector {
111
112
  isRunning = false;
112
113
  /** Key of the last detected dialog (for duplicate notification prevention) */
113
114
  lastDetectedKey = null;
114
- /** Full RunCommandInfo from the last detection (used for clicking) */
115
+ /** Full RunCommandInfo from the last detection */
115
116
  lastDetectedInfo = null;
117
+ /** Gate for empty polls before reset */
118
+ emptyPollGate = new consecutiveEmptyPollGate_1.ConsecutiveEmptyPollGate(3);
116
119
  constructor(options) {
117
120
  this.cdpService = options.cdpService;
118
121
  this.pollIntervalMs = options.pollIntervalMs ?? 1500;
@@ -126,6 +129,7 @@ class RunCommandDetector {
126
129
  this.isRunning = true;
127
130
  this.lastDetectedKey = null;
128
131
  this.lastDetectedInfo = null;
132
+ this.emptyPollGate.reset();
129
133
  this.schedulePoll();
130
134
  }
131
135
  /** Stop monitoring. */
@@ -171,6 +175,7 @@ class RunCommandDetector {
171
175
  const result = await this.cdpService.call('Runtime.evaluate', callParams);
172
176
  const info = result?.result?.value ?? null;
173
177
  if (info) {
178
+ this.emptyPollGate.recordDetection();
174
179
  // Duplicate prevention: use commandText as key
175
180
  const key = `${info.commandText}::${info.workingDirectory}`;
176
181
  if (key !== this.lastDetectedKey) {
@@ -180,11 +185,13 @@ class RunCommandDetector {
180
185
  }
181
186
  }
182
187
  else {
183
- const wasDetected = this.lastDetectedKey !== null;
184
- this.lastDetectedKey = null;
185
- this.lastDetectedInfo = null;
186
- if (wasDetected && this.onResolved) {
187
- this.onResolved();
188
+ if (this.emptyPollGate.recordEmptyPoll()) {
189
+ const wasDetected = this.lastDetectedKey !== null;
190
+ this.lastDetectedKey = null;
191
+ this.lastDetectedInfo = null;
192
+ if (wasDetected && this.onResolved) {
193
+ this.onResolved();
194
+ }
188
195
  }
189
196
  }
190
197
  }
@@ -11,6 +11,7 @@ exports.sendArtifactsUI = sendArtifactsUI;
11
11
  exports.sendArtifactPickerUI = sendArtifactPickerUI;
12
12
  const discord_js_1 = require("discord.js");
13
13
  const artifactService_1 = require("../services/artifactService");
14
+ const pathUtils_1 = require("../utils/pathUtils");
14
15
  // ---------------------------------------------------------------------------
15
16
  // Constants
16
17
  // ---------------------------------------------------------------------------
@@ -143,7 +144,8 @@ async function sendArtifactPickerUI(interaction, deps, edit = false, resolvedCon
143
144
  // Fallback to title matching if ID is not in DB or doesn't match a real folder
144
145
  if (!conversationId || !artifactService.listArtifacts(conversationId).length) {
145
146
  const sessionTitle = session.displayName?.trim() ?? '';
146
- const matchedId = sessionTitle ? artifactService.findConversationByTitle(sessionTitle) : null;
147
+ const workspaceDirName = (0, pathUtils_1.getWorkspaceDirName)(session);
148
+ const matchedId = sessionTitle ? artifactService.findConversationByTitle(sessionTitle, workspaceDirName) : null;
147
149
  if (matchedId)
148
150
  conversationId = matchedId;
149
151
  }
@@ -151,8 +153,7 @@ async function sendArtifactPickerUI(interaction, deps, edit = false, resolvedCon
151
153
  // to avoid showing artifacts from other projects/chats.
152
154
  }
153
155
  else {
154
- // Final fallback: latest overall (only for non-session channels)
155
- conversationId = artifactService.getLatestConversationWithArtifacts();
156
+ conversationId = null;
156
157
  }
157
158
  }
158
159
  if (!conversationId) {
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConsecutiveEmptyPollGate = void 0;
4
+ class ConsecutiveEmptyPollGate {
5
+ requiredEmptyPolls;
6
+ count = 0;
7
+ constructor(requiredEmptyPolls = 3) {
8
+ this.requiredEmptyPolls = requiredEmptyPolls;
9
+ }
10
+ recordDetection() {
11
+ this.count = 0;
12
+ }
13
+ recordEmptyPoll() {
14
+ this.count++;
15
+ return this.count >= this.requiredEmptyPolls;
16
+ }
17
+ reset() {
18
+ this.count = 0;
19
+ }
20
+ }
21
+ exports.ConsecutiveEmptyPollGate = ConsecutiveEmptyPollGate;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fileOpenCache = void 0;
4
+ /**
5
+ * In-memory cache to map button customIds to file URLs.
6
+ * Bounded to a maximum size to prevent memory leaks, and entries are removed once consumed.
7
+ */
8
+ class BoundedCache {
9
+ max_size;
10
+ map;
11
+ constructor(max_size = 1000) {
12
+ this.max_size = max_size;
13
+ this.map = new Map();
14
+ }
15
+ set(key, value) {
16
+ if (this.map.size >= this.max_size) {
17
+ // Remove the oldest entry (first item in the map's insertion order)
18
+ const firstKey = this.map.keys().next().value;
19
+ if (firstKey !== undefined) {
20
+ this.map.delete(firstKey);
21
+ }
22
+ }
23
+ this.map.set(key, value);
24
+ }
25
+ get(key) {
26
+ const value = this.map.get(key);
27
+ if (value !== undefined) {
28
+ this.map.delete(key); // Remove upon consumption
29
+ }
30
+ return value;
31
+ }
32
+ }
33
+ exports.fileOpenCache = new BoundedCache(1000);
@@ -42,6 +42,9 @@ function htmlToDiscordMarkdown(html) {
42
42
  // Extract language from class="language-xxx" if present.
43
43
  // Do NOT decode entities here — let the final decodeEntities() handle them
44
44
  // after stripTags() has run, to avoid decoded < > being stripped as tags.
45
+ // First, Antigravity 2.0 wrapped code blocks
46
+ result = result.replace(/<div[^>]*class="[^"]*code-block[^"]*"[^>]*>(?:(?!<div[^>]*class="[^"]*code-block[^"]*"[^>]*>)[\s\S])*?<div[^>]*class="[^"]*code-header[^"]*"[^>]*>([\s\S]*?)<\/div>(?:(?!<div[^>]*class="[^"]*code-block[^"]*"[^>]*>)[\s\S])*?<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>(?:(?!<div[^>]*class="[^"]*code-block[^"]*"[^>]*>)[\s\S])*?<\/div>/gi, (_m, lang, content) => `\n\`\`\`${stripTags(lang).trim()}\n${content}\n\`\`\`\n`);
47
+ // Standard <pre><code> blocks
45
48
  result = result.replace(/<pre[^>]*>\s*<code(?:\s+class="language-([^"]*)")?[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, lang, content) => `\n\`\`\`${lang || ''}\n${content}\n\`\`\`\n`);
46
49
  // Handle inline <code>
47
50
  result = result.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
@@ -49,8 +52,8 @@ function htmlToDiscordMarkdown(html) {
49
52
  result = result.replace(/<(?:strong|b)(?:\s[^>]*)?>((?: |\s|[^<]|<(?!\/(?:strong|b)>))*)<\/(?:strong|b)>/gi, '**$1**');
50
53
  // Handle <em> and <i>
51
54
  result = result.replace(/<(?:em|i)(?:\s[^>]*)?>((?: |\s|[^<]|<(?!\/(?:em|i)>))*)<\/(?:em|i)>/gi, '*$1*');
52
- // Handle <span class="context-scope-mention"> → `text`
53
- result = result.replace(/<span[^>]*class="[^"]*context-scope-mention[^"]*"[^>]*>([\s\S]*?)<\/span>/gi, (_m, text) => `\`${stripTags(text).trim()}\``);
55
+ // Handle <span class="context-scope-mention"> or <span class="file-chip"> → `text`
56
+ result = result.replace(/<span[^>]*class="[^"]*(?:context-scope-mention|file-chip)[^"]*"[^>]*>([\s\S]*?)<\/span>/gi, (_m, text) => `\`${stripTags(text).trim()}\``);
54
57
  // Handle elements with title attribute containing file paths
55
58
  // e.g. <div title="src/bot/index.ts">:54</div> → src/bot/index.ts:54
56
59
  result = result.replace(/<(?:div|span|a)[^>]*\btitle="([^"]*)"[^>]*>([\s\S]*?)<\/(?:div|span|a)>/gi, (_m, title, text) => {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getAntigravityCliPath = getAntigravityCliPath;
4
4
  exports.extractProjectNameFromPath = extractProjectNameFromPath;
5
5
  exports.getAntigravityCdpHint = getAntigravityCdpHint;
6
+ exports.getWorkspaceDirName = getWorkspaceDirName;
6
7
  /**
7
8
  * Helper to resolve the correct Antigravity CLI executable path based on the operating system
8
9
  * and environment variables.
@@ -55,3 +56,14 @@ function getAntigravityCdpHint(port = 9222) {
55
56
  return `${APP_NAME.toLowerCase()} --remote-debugging-port=${port}`;
56
57
  }
57
58
  }
59
+ /**
60
+ * Extracts the workspace directory name from a chat session.
61
+ * @param session The chat session object
62
+ * @returns The base directory name, or undefined
63
+ */
64
+ function getWorkspaceDirName(session) {
65
+ if (session && session.workspacePath) {
66
+ return extractProjectNameFromPath(session.workspacePath);
67
+ }
68
+ return undefined;
69
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProjectName = resolveProjectName;
4
+ function resolveProjectName(deps, channelId, providedProjectName) {
5
+ if (providedProjectName) {
6
+ return providedProjectName;
7
+ }
8
+ const workspacePath = deps.wsHandler.getWorkspaceForChannel(channelId);
9
+ return workspacePath ? deps.bridge.pool.extractProjectName(workspacePath) : undefined;
10
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseQuestionCustomId = parseQuestionCustomId;
4
+ function parseQuestionCustomId(customId, expectedPrefix) {
5
+ if (customId !== expectedPrefix && !customId.startsWith(expectedPrefix + ':'))
6
+ return null;
7
+ const parts = customId.split(':');
8
+ const result = { action: parts[0] };
9
+ try {
10
+ if (parts.length > 1) {
11
+ result.projectName = decodeURIComponent(parts[1]);
12
+ }
13
+ if (parts.length > 2) {
14
+ result.channelId = decodeURIComponent(parts[2]);
15
+ }
16
+ }
17
+ catch (e) {
18
+ // Fallback in case of malformed URI component from truncation
19
+ if (parts.length > 1)
20
+ result.projectName = parts[1];
21
+ if (parts.length > 2)
22
+ result.channelId = parts[2];
23
+ }
24
+ return result;
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazy-gravity",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "Control Antigravity from anywhere — a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {