From 0da6291d989f3b428e1617e3e638df736f05e7bc Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Tue, 17 Mar 2026 20:36:27 +0900 Subject: [PATCH] chore(extension): bump to v0.4.4 - dual delivery fix + echo dedup --- extension/out/extension.js | 752 ++------------------------------- extension/out/extension.js.map | 2 +- extension/package.json | 2 +- 3 files changed, 40 insertions(+), 716 deletions(-) diff --git a/extension/out/extension.js b/extension/out/extension.js index 79fb8f9..ccad8c5 100644 --- a/extension/out/extension.js +++ b/extension/out/extension.js @@ -51,10 +51,11 @@ const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const cp = __importStar(require("child_process")); -const crypto = __importStar(require("crypto")); const ws_client_1 = require("./ws-client"); -const observer_script_1 = require("./observer-script"); const step_probe_1 = require("./step-probe"); +const http_bridge_1 = require("./http-bridge"); +const html_patcher_1 = require("./html-patcher"); +const command_handler_1 = require("./command-handler"); // ─── File-based logging (AI can read directly) ─── function logToFile(msg) { const ts = new Date().toISOString().replace('T', ' ').substring(0, 19); @@ -81,9 +82,7 @@ let projectName; let workspaceUri = ''; // filesystem path of the workspace folder (for session filtering) let isActive = false; let autoApproveEnabled = false; // toggled via !auto from Discord -let deterministicPort = 0; // derived from projectName, consistent across restarts let watcher = null; -let commandsWatcher = null; let wsBridge = null; // WebSocket Hub connection const sentPendingIds = new Set(); // Memory-based dedup: tracks recently created pending step_indexes to prevent @@ -214,103 +213,7 @@ function writeChatSnapshotWithFiles(text, files) { console.log(`Gravity Bridge: snapshot+files write error: ${e.message}`); } } -// ─── Command File Watcher (Discord → Antigravity) ─── -function processCommandFile(filePath) { - 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 !== projectName) { - console.log(`Gravity Bridge: skipping command for "${cmd.project_name}" (we are "${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' && sdk) { - sdk.cascade.acceptStep().catch((e) => console.log(`Gravity Bridge: approve error: ${e.message}`)); - } - else if (action === 'reject' && sdk) { - sdk.cascade.rejectStep().catch((e) => console.log(`Gravity Bridge: reject error: ${e.message}`)); - } - else if (action === 'approve_terminal' && sdk) { - sdk.cascade.acceptTerminalCommand().catch((e) => 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 - if (text === '!auto on') { - autoApproveEnabled = true; - } - else if (text === '!auto off') { - autoApproveEnabled = false; - } - else { - // Toggle if no explicit on/off - autoApproveEnabled = !autoApproveEnabled; - } - logToFile(`[AUTO] auto-approve toggled → ${autoApproveEnabled}`); - } - else if (text) { - // Send message to Antigravity — use VS Code command (most reliable) - 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) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`)); - } - // Remove processed command file - try { - fs.unlinkSync(filePath); - } - catch { } - } - catch (e) { - console.log(`Gravity Bridge: command processing error: ${e.message}`); - } -} -function watchCommandsDir() { - const cmdDir = path.join(bridgePath, 'commands'); - // Process existing files - const processAllCommands = () => { - try { - for (const f of fs.readdirSync(cmdDir)) { - if (f.endsWith('.json')) { - processCommandFile(path.join(cmdDir, f)); - } - } - } - 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), 200); - } - } - }); - } - catch { } - // Polling fallback: fs.watch on Windows can silently fail - setInterval(() => { - processAllCommands(); - }, 3000); -} +// ─── Command handling extracted to ./command-handler.ts ─── // ─── SDK Integration ─── async function initSDK(context) { try { @@ -469,574 +372,13 @@ async function fixLSConnection() { logToFile(`[LS-FIX] error: ${err.message}`); } } -// ─── Approval Observer via SDK IntegrationManager ─── -async function setupApprovalObserver() { - if (!sdk) { - logToFile('[OBSERVER] no SDK'); - return; - } - try { - const integration = sdk.integration; - if (!integration) { - logToFile('[OBSERVER] sdk.integration unavailable'); - return; - } - // 1. Start HTTP bridge server in Extension Host - const bridgePort = await startObserverHttpBridge(); - if (!bridgePort) { - logToFile('[OBSERVER] HTTP bridge failed'); - return; - } - // 2. Register a TOP_BAR button so build() works - try { - integration.register({ - id: 'gravity_bridge_status', - point: 'topBar', - icon: '🌉', - tooltip: 'Gravity Bridge Active', - }); - } - catch { /* already registered */ } - // 3. Write renderer script with HTTP fetch() approach - const observerJS = (0, observer_script_1.generateApprovalObserverScript)(bridgePort); - const patcher = integration._patcher; - if (patcher && typeof patcher.getScriptPath === 'function') { - let baseScript = ''; - try { - baseScript = integration.build(); - } - catch { - baseScript = ''; - } - const combinedScript = baseScript + '\n' + observerJS; - const scriptPath = patcher.getScriptPath(); - fs.writeFileSync(scriptPath, combinedScript, 'utf8'); - logToFile(`[OBSERVER] script written → ${scriptPath} (port=${bridgePort})`); - if (!integration.isInstalled()) { - patcher.install(combinedScript); - logToFile('[OBSERVER] patcher.install() called (needs reload)'); - } - // Patch BOTH HTML files with inline script injection. - // CRITICAL: vscode-file:// does NOT serve custom .js files (silent 404), - // so we MUST inline the script directly into BOTH HTML files. - // workbench.html — loaded by DevTools/standard mode - // workbench-jetski-agent.html — loaded by AG agent mode - const scriptDir = path.dirname(scriptPath); - // Each HTML file has DIFFERENT CSS/JS entry points — they are NOT interchangeable: - // workbench.html → workbench.desktop.main.css + workbench.js - // workbench-jetski-agent.html → tw-base.tailwind.css + jetskiMain.tailwind.css + jetskiAgent.js - // Cross-restoring between them causes CSS to not load → layout broken (elements visible but all shifted left). - const htmlFileSpecs = [ - { - name: 'workbench.html', - requiredMarker: 'workbench.desktop.main.css', // CSS unique to this file - requiredScript: 'workbench.js', // JS entry point - }, - { - name: 'workbench-jetski-agent.html', - requiredMarker: 'jetskiMain.tailwind.css', // CSS unique to this file - requiredScript: 'jetskiAgent.js', // JS entry point - }, - ]; - // ── FIX #1: File lock to prevent multi-instance HTML patching race ── - const lockFile = path.join(scriptDir, '.patch-lock'); - let lockAcquired = false; - try { - if (fs.existsSync(lockFile)) { - const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs; - if (lockAge < 30_000) { - logToFile(`[OBSERVER] another instance is patching (lock age=${Math.round(lockAge / 1000)}s) — skipping`); - return; // Exit setupApprovalObserver entirely - } - logToFile(`[OBSERVER] stale lock (age=${Math.round(lockAge / 1000)}s) — force-acquiring`); - } - fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, ts: Date.now() }), 'utf-8'); - lockAcquired = true; - } - catch (lockErr) { - logToFile(`[OBSERVER] lock acquire error: ${lockErr.message} — proceeding anyway`); - } - for (const spec of htmlFileSpecs) { - const htmlPath = path.join(scriptDir, spec.name); - const backupPath = htmlPath + '.orig'; - try { - if (!fs.existsSync(htmlPath)) { - logToFile(`[OBSERVER] ${spec.name} not found — skipping`); - continue; - } - let html = fs.readFileSync(htmlPath, 'utf8'); - // ── BACKUP: Save original before first-ever patch ── - // Only backup if the file looks valid AND hasn't been backed up yet. - if (!fs.existsSync(backupPath) - && html.length >= 500 - && html.includes('') - && html.includes(spec.requiredMarker)) { - fs.writeFileSync(backupPath, html, 'utf8'); - logToFile(`[OBSERVER] ${spec.name} backed up to .orig (${html.length} bytes)`); - } - // ── SAFETY: Refuse to patch if file is corrupt, empty, or wrong type ── - // Race condition: another extension instance may be mid-write (0-byte). - // Wrong type: restored from the other HTML file (different CSS/JS refs). - const isCorrupt = html.length < 500 || !html.includes(''); - const isWrongType = !isCorrupt && !html.includes(spec.requiredMarker); - if (isCorrupt || isWrongType) { - const reason = isCorrupt - ? `corrupt/empty (${html.length} bytes)` - : `wrong type (missing ${spec.requiredMarker})`; - logToFile(`[OBSERVER] ${spec.name} detected ${reason}`); - // Try to restore from backup - if (fs.existsSync(backupPath)) { - const backup = fs.readFileSync(backupPath, 'utf8'); - if (backup.length >= 500 - && backup.includes('') - && backup.includes(spec.requiredMarker)) { - fs.writeFileSync(htmlPath, backup, 'utf8'); - html = backup; - logToFile(`[OBSERVER] ${spec.name} RESTORED from .orig backup (${backup.length} bytes) ✅`); - } - else { - logToFile(`[OBSERVER] ${spec.name} .orig backup also invalid — SKIPPING`); - continue; - } - } - else { - logToFile(`[OBSERVER] ${spec.name} no .orig backup available — SKIPPING to prevent further damage`); - continue; - } - } - // CRITICAL: Patch CSP to allow inline scripts. - // Default CSP has script-src 'self' 'unsafe-eval' blob: — NO 'unsafe-inline'. - // Without 'unsafe-inline', all inline \n${inlineMarkerEnd}`); - logToFile(`[OBSERVER] ${spec.name} inline script UPDATED`); - } - else { - html = html.replace('', `\n${inlineMarkerStart}\n\n${inlineMarkerEnd}\n`); - logToFile(`[OBSERVER] ${spec.name} inline script INSERTED`); - } - // SAFETY: Final validation before write - if (html.length < 500 || !html.includes('') || !html.includes(spec.requiredMarker)) { - logToFile(`[OBSERVER] ${spec.name} WOULD BE CORRUPT after patching (${html.length} bytes) — ABORTING write`); - continue; - } - fs.writeFileSync(htmlPath, html, 'utf8'); - } - catch (e) { - logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`); - } - } - // Release patch lock - if (lockAcquired) { - try { - fs.unlinkSync(lockFile); - } - catch { } - logToFile('[OBSERVER] patch lock released'); - } - } - // 4. Update product.json checksums so vscode-file:// serves our patched files - updateProductChecksums(); - try { - integration.enableAutoRepair(); - } - catch { } - setInterval(() => { try { - integration.signalActive(); - } - catch { } }, 30_000); - logToFile(`[OBSERVER] setup complete (HTTP bridge on port ${bridgePort})`); - console.log(`Gravity Bridge: ✅ Approval observer installed (port ${bridgePort})`); - } - catch (err) { - logToFile(`[OBSERVER] setup error: ${err.message}`); - } -} -// ─── Product.json Checksum Auto-Update ─── -// vscode-file:// protocol validates SHA256 checksums in product.json. -// If a file's checksum doesn't match, Electron serves the ORIGINAL cached version. -// This function recalculates checksums for files we modify (HTML files with