refactor(extension): split extension.ts into 3 modules - http-bridge, html-patcher, command-handler (#398)
This commit is contained in:
189
extension/src/command-handler.ts
Normal file
189
extension/src/command-handler.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Command Handler — Discord → AG command processing.
|
||||
*
|
||||
* Extracted from extension.ts to reduce file size.
|
||||
* Handles commands from both:
|
||||
* - File-based bridge (legacy/fallback): fs.watch + polling
|
||||
* - WebSocket Hub (primary): direct callbacks
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// ─── Context interface (shared state from extension.ts) ───
|
||||
|
||||
export interface CommandHandlerContext {
|
||||
bridgePath: string;
|
||||
projectName: string;
|
||||
sdk: any;
|
||||
autoApproveEnabled: boolean;
|
||||
logToFile: (msg: string) => void;
|
||||
/** Called when auto-approve is toggled; extension.ts updates its own state */
|
||||
onAutoApproveChanged: (enabled: boolean) => void;
|
||||
/** Track recently sent Discord→AG texts to avoid echo relay */
|
||||
recentDiscordSentTexts: Map<string, number>;
|
||||
}
|
||||
|
||||
// ─── File-based command watcher ───
|
||||
|
||||
let commandsWatcher: fs.FSWatcher | null = null;
|
||||
|
||||
/**
|
||||
* Watch the commands directory for Discord→AG command files.
|
||||
* Uses fs.watch + polling fallback (fs.watch unreliable on Windows).
|
||||
*/
|
||||
export function watchCommandsDir(ctx: CommandHandlerContext) {
|
||||
const cmdDir = path.join(ctx.bridgePath, 'commands');
|
||||
|
||||
// Process existing files
|
||||
const processAllCommands = () => {
|
||||
try {
|
||||
for (const f of fs.readdirSync(cmdDir)) {
|
||||
if (f.endsWith('.json')) {
|
||||
_processCommandFile(path.join(cmdDir, f), ctx);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
};
|
||||
|
||||
processAllCommands();
|
||||
|
||||
// Watch for new files (may not fire reliably on Windows)
|
||||
try {
|
||||
commandsWatcher = fs.watch(cmdDir, (event, filename) => {
|
||||
if (filename && filename.endsWith('.json') && event === 'rename') {
|
||||
const fp = path.join(cmdDir, filename);
|
||||
if (fs.existsSync(fp)) {
|
||||
setTimeout(() => _processCommandFile(fp, ctx), 200);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
|
||||
// Polling fallback: fs.watch on Windows can silently fail
|
||||
setInterval(() => {
|
||||
processAllCommands();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/** Dispose the file watcher on deactivate */
|
||||
export function disposeCommandsWatcher() {
|
||||
if (commandsWatcher) {
|
||||
commandsWatcher.close();
|
||||
commandsWatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── WebSocket command handler ───
|
||||
|
||||
/**
|
||||
* Handle a command received via WebSocket Hub.
|
||||
* Same logic as file-based processing but without file I/O.
|
||||
*/
|
||||
export function handleWSCommand(ctx: CommandHandlerContext, data: { text?: string; action?: string; project_name?: string }) {
|
||||
const text = data.text || '';
|
||||
if (!text) return;
|
||||
|
||||
// Project filtering (WS already routes by project, but double-check)
|
||||
if (data.project_name && data.project_name !== ctx.projectName) {
|
||||
ctx.logToFile(`[WS-CMD] Ignoring command for ${data.project_name} (we are ${ctx.projectName})`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text === '!stop') {
|
||||
ctx.logToFile('[WS-CMD] !stop — cancelling AG task');
|
||||
if (ctx.sdk) {
|
||||
try { ctx.sdk.cascade.cancelCurrentTask(); } catch { }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.startsWith('!auto')) {
|
||||
const parts = text.split(' ');
|
||||
const enabled = parts[1] !== 'off';
|
||||
ctx.onAutoApproveChanged(enabled);
|
||||
ctx.logToFile(`[WS-CMD] auto_approve=${enabled}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// General text → send as user message to AG
|
||||
ctx.logToFile(`[WS-CMD] Sending text to AG: ${text.substring(0, 80)}`);
|
||||
if (ctx.sdk) {
|
||||
try {
|
||||
ctx.sdk.cascade.sendPrompt(text);
|
||||
} catch (e: any) {
|
||||
ctx.logToFile(`[WS-CMD] SDK sendPrompt error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Private ───
|
||||
|
||||
function _processCommandFile(filePath: string, ctx: CommandHandlerContext) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const cmd = JSON.parse(content);
|
||||
|
||||
// Skip already consumed commands
|
||||
if (cmd.consumed) {
|
||||
try { fs.unlinkSync(filePath); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore commands for other projects
|
||||
if (cmd.project_name && cmd.project_name !== ctx.projectName) {
|
||||
console.log(`Gravity Bridge: skipping command for "${cmd.project_name}" (we are "${ctx.projectName}")`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bot writes 'text' field, not 'message'
|
||||
const text = cmd.text || cmd.message || '';
|
||||
const action = cmd.action || '';
|
||||
|
||||
console.log(`Gravity Bridge: command — text="${text}" action="${action}"`);
|
||||
|
||||
if (action === 'approve' && ctx.sdk) {
|
||||
ctx.sdk.cascade.acceptStep().catch((e: any) =>
|
||||
console.log(`Gravity Bridge: approve error: ${e.message}`)
|
||||
);
|
||||
} else if (action === 'reject' && ctx.sdk) {
|
||||
ctx.sdk.cascade.rejectStep().catch((e: any) =>
|
||||
console.log(`Gravity Bridge: reject error: ${e.message}`)
|
||||
);
|
||||
} else if (action === 'approve_terminal' && ctx.sdk) {
|
||||
ctx.sdk.cascade.acceptTerminalCommand().catch((e: any) =>
|
||||
console.log(`Gravity Bridge: approve_terminal error: ${e.message}`)
|
||||
);
|
||||
} else if (text === '!stop') {
|
||||
// Cancel current operation
|
||||
vscode.commands.executeCommand('antigravity.agent.rejectAgentStep')
|
||||
.then(() => console.log('Gravity Bridge: ✅ stop sent'),
|
||||
() => { });
|
||||
} else if (text.startsWith('!auto')) {
|
||||
// Auto-approve mode toggle
|
||||
let enabled: boolean;
|
||||
if (text === '!auto on') {
|
||||
enabled = true;
|
||||
} else if (text === '!auto off') {
|
||||
enabled = false;
|
||||
} else {
|
||||
// Toggle if no explicit on/off
|
||||
enabled = !ctx.autoApproveEnabled;
|
||||
}
|
||||
ctx.onAutoApproveChanged(enabled);
|
||||
ctx.logToFile(`[AUTO] auto-approve toggled → ${enabled}`);
|
||||
} else if (text) {
|
||||
// Send message to Antigravity — use VS Code command (most reliable)
|
||||
ctx.recentDiscordSentTexts.set(text.trim(), Date.now());
|
||||
vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', text)
|
||||
.then(() => console.log(`Gravity Bridge: ✅ sent "${text.substring(0, 50)}" via sendPromptToAgentPanel`),
|
||||
(e: any) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`));
|
||||
}
|
||||
|
||||
// Remove processed command file
|
||||
try { fs.unlinkSync(filePath); } catch { }
|
||||
} catch (e: any) {
|
||||
console.log(`Gravity Bridge: command processing error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user