Skip to content
Draft
17 changes: 7 additions & 10 deletions docs/ANTIGRAVITY_DOM_SELECTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ The assistant's response body, rendered with markdown formatting.
| 10 | `.rendered-markdown` | **Verified** | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 9 | `.leading-relaxed.select-text` | **Verified** | `responseMonitor.ts`, `planningDetector.ts`, `assistantDomExtractor.ts` |
| 8 | `.flex.flex-col.gap-y-3` | **Deprecated** — removed from primary extraction paths (AG 1.23.x) | — |
| 7 | `[data-message-author-role="assistant"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 6 | `[data-message-role="assistant"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 5 | `[class*="assistant-message"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 4 | `[class*="message-content"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 3 | `[class*="markdown-body"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| | `[data-message-author-role="assistant"]` | **Removed** (Issue #40 cleanup) | — |
| | `[data-message-role="assistant"]` | **Removed** (Issue #40 cleanup) | — |
| | `[class*="assistant-message"]` | **Removed** (Issue #40 cleanup) | — |
| | `[class*="message-content"]` | **Removed** (Issue #40 cleanup) | — |
| | `[class*="markdown-body"]` | **Removed** (Issue #40 cleanup) | — |
| 2 | `.prose` | Unverified | `responseMonitor.ts`, `assistantDomExtractor.ts` |

> **Note**: Selectors scored 3-7 appear to be inherited from ChatGPT/generic patterns and do **not** exist in Antigravity's DOM. They are harmless (scored lower, never matched) but add noise. The top selectors (`.text-ide-message-block-bot-color`, `.rendered-markdown`, `.leading-relaxed.select-text`) are the ones that actually match.
> **Note**: Selectors scored 3-7 inherited from ChatGPT/generic patterns were removed in Issue #40 cleanup. The active selectors (`.text-ide-message-block-bot-color`, `.rendered-markdown`, `.leading-relaxed.select-text`, `.prose`) handle response extraction.

### Exclusion Containers

Expand Down Expand Up @@ -246,10 +246,7 @@ Model quota reached / rate limit detection.
Quota text is only matched outside response containers to avoid false positives:

```
.rendered-markdown, .prose, pre, code,
[data-message-author-role="assistant"],
[data-message-role="assistant"],
[class*="message-content"]
.rendered-markdown, .prose, pre, code
```

---
Expand Down
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,6 @@
"Fast Mode — for simple tasks": "Fast Mode — for simple tasks",
"Plan Mode — for complex step-by-step tasks": "Plan Mode — for complex step-by-step tasks",
"⚠️ Mode name not specified. Available modes: ": "⚠️ Mode name not specified. Available modes: ",
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}"
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}",
"📂 Artifacts": "📂 Artifacts"
}
3 changes: 2 additions & 1 deletion locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,6 @@
"Fast Mode — for simple tasks": "高速応答モード — シンプルなタスク向け",
"Plan Mode — for complex step-by-step tasks": "計画モード — 複雑なタスクを段階的に実行",
"⚠️ Mode name not specified. Available modes: ": "⚠️ モード名が指定されていません。利用可能なモード: ",
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ 無効なモード \"${modeName}\" です。利用可能なモード: ${AVAILABLE_MODES.join(', ')}"
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ 無効なモード \"${modeName}\" です。利用可能なモード: ${AVAILABLE_MODES.join(', ')}",
"📂 Artifacts": "📂 成果物一覧"
}
46 changes: 40 additions & 6 deletions src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ import {
import { buildModeModelLines, fitForSingleEmbedDescription, splitForEmbedDescription } from '../utils/streamMessageFormatter';
import { formatForDiscord, splitOutputAndLogs } from '../utils/discordFormatter';
import { renderDiscordResponse } from '../platform/discord/discordResponseRenderer';
import { ProcessLogBuffer } from '../utils/processLogBuffer';
import { ProcessLogBuffer, createDefaultProcessLogBuffer } from '../utils/processLogBuffer';
import {
buildPromptWithAttachmentUrls,
cleanupInboundImageAttachments,
Expand Down Expand Up @@ -453,10 +453,8 @@ async function sendPromptToAntigravity(
let lastActivityLogText = '';
const LIVE_RESPONSE_MAX_LEN = 3800;
const LIVE_ACTIVITY_MAX_LEN = 3800;
const processLogBuffer = new ProcessLogBuffer({
const processLogBuffer = createDefaultProcessLogBuffer({
maxChars: LIVE_ACTIVITY_MAX_LEN,
maxEntries: 120,
maxEntryLength: 220,
});
const liveResponseMessages: any[] = [];
const liveActivityMessages: any[] = [];
Expand Down Expand Up @@ -3094,12 +3092,48 @@ export async function handleSlashInteraction(
const response = await fetch(attachment.url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

const MAX_BYTES = 1024 * 1024; // 1 MiB
const contentLength = response.headers.get('content-length');
if (contentLength && parseInt(contentLength, 10) > 1024 * 1024) {
if (contentLength && parseInt(contentLength, 10) > MAX_BYTES) {
throw new Error('Response body exceeds maximum size limit of 1MB.');
}

jsonText = await response.text();
if (response.body && typeof (response.body as any).getReader === 'function') {
const reader = (response.body as any).getReader();
const chunks: Uint8Array[] = [];
let totalBytes = 0;

try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
totalBytes += value.length;
if (totalBytes > MAX_BYTES) {
controller.abort();
throw new Error('Response body exceeds maximum size limit of 1MB.');
}
chunks.push(value);
}
}
} finally {
if (reader.releaseLock) reader.releaseLock();
}

const combined = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
jsonText = new TextDecoder().decode(combined);
} else {
const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength > MAX_BYTES) {
throw new Error('Response body exceeds maximum size limit of 1MB.');
}
jsonText = new TextDecoder().decode(arrayBuffer);
}
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
Expand Down
4 changes: 2 additions & 2 deletions src/bot/telegramMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { WorkspaceService } from '../services/workspaceService';
import { CdpBridge, registerApprovalWorkspaceChannel, ensureApprovalDetector, ensureErrorPopupDetector, ensurePlanningDetector, ensureRunCommandDetector, ensureQuestionDetector } from '../services/cdpBridgeManager';
import { CdpService } from '../services/cdpService';
import { ResponseMonitor, captureResponseMonitorBaseline } from '../services/responseMonitor';
import { ProcessLogBuffer } from '../utils/processLogBuffer';
import { ProcessLogBuffer, createDefaultProcessLogBuffer } from '../utils/processLogBuffer';
import { splitOutputAndLogs } from '../utils/discordFormatter';
import { parseTelegramProjectCommand, handleTelegramProjectCommand } from './telegramProjectCommand';
import { parseTelegramCommand, handleTelegramCommand } from './telegramCommands';
Expand Down Expand Up @@ -297,7 +297,7 @@ export function createTelegramMessageHandler(deps: TelegramMessageHandlerDeps) {
// Monitor the response
const channel = message.channel;
const startTime = Date.now();
const processLogBuffer = new ProcessLogBuffer({ maxChars: 3500, maxEntries: 120, maxEntryLength: 220 });
const processLogBuffer = createDefaultProcessLogBuffer();
let lastActivityLogText = '';
let statusMsg: PlatformSentMessage | null = null;

Expand Down
23 changes: 17 additions & 6 deletions src/commands/joinCommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export class JoinCommandHandler {
}

ensureUserMessageDetector(bridge, cdp, projectName, (info) => {
this.routeMirroredMessage(cdp, projectName, info)
this.routeMirroredMessage(cdp, projectName, info, bridge)
.catch((err) => {
logger.error('[Mirror] Error routing mirrored message:', err);
});
Expand All @@ -323,13 +323,14 @@ export class JoinCommandHandler {
* Route a mirrored PC message to the correct Discord channel and
* start a passive ResponseMonitor to capture the AI response.
*
* Routing: chatSessionRepo.findByDisplayName only — no fallbacks.
* Sessions without an explicit channel binding are silently skipped.
* Routing: chatSessionRepo.findByDisplayName first, falling back to
* the project workspace approval channel if no session binding exists.
*/
private async routeMirroredMessage(
cdp: CdpService,
projectName: string,
info: { text: string },
bridge?: CdpBridge,
): Promise<void> {
const chatTitle = await getCurrentChatTitle(cdp);

Expand All @@ -339,12 +340,22 @@ export class JoinCommandHandler {
}

const session = this.chatSessionRepo.findByDisplayName(projectName, chatTitle);
if (!session) {
logger.debug(`[Mirror] No bound channel for session "${chatTitle}", skipping`);
let targetChannelId = session?.channelId;

// Fallback to workspace approval channel if no session-specific channel binding exists
if (!targetChannelId && bridge) {
const workspaceChannel = bridge.approvalChannelByWorkspace.get(projectName);
if (workspaceChannel) {
targetChannelId = workspaceChannel.id;
}
}

if (!targetChannelId) {
logger.debug(`[Mirror] No bound channel for session "${chatTitle}" or workspace "${projectName}", skipping`);
return;
}

const channel = this.client.channels.cache.get(session.channelId);
const channel = this.client.channels.cache.get(targetChannelId);
if (!channel || !('send' in channel)) return;
const sendable = channel as { send: (...args: any[]) => Promise<any> };

Expand Down
Loading