alfred-commander/extension.js
Commander bac008c14a feat: Alfred Commander v1.0.1 — 3551 lines of AI chat, voice commands, workspace tools
- AI chat with multi-provider support (Anthropic, OpenAI, Groq)
- Voice command recognition and TTS
- File explorer, terminal integration
- Account stats and usage tracking
- Session management with token auth
- Webview-based chat panel with markdown rendering
2026-04-07 15:47:17 -04:00

3552 lines
171 KiB
JavaScript

const vscode = require('vscode');
const https = require('https');
const http = require('http');
const os = require('os');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { execSync } = require('child_process');
let currentAgent = 'alfred';
let convId = null;
let csrfToken = null;
let sessionCookie = null;
let userProfile = null;
let hmacSecretCache = null;
// ═══════════════════════════════════════════════════════════════════════════
// WORKSPACE INTELLIGENCE ENGINE — Deep awareness of project, files, git
// ═══════════════════════════════════════════════════════════════════════════
function getWorkspaceRoot() {
const folders = vscode.workspace.workspaceFolders;
return folders && folders.length > 0 ? folders[0].uri.fsPath : os.homedir();
}
function safeExec(cmd, cwd, timeout) {
try {
return execSync(cmd, { cwd: cwd || getWorkspaceRoot(), encoding: 'utf8', timeout: timeout || 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
} catch (_) { return ''; }
}
function getProjectStructure() {
const root = getWorkspaceRoot();
try {
const tree = safeExec('find . -maxdepth 3 -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.venv/*" -not -path "./__pycache__/*" -not -path "./vendor/*" -not -path "./dist/*" -not -path "./build/*" | sort | head -200', root);
return tree || '';
} catch (_) { return ''; }
}
function detectProjectType() {
const root = getWorkspaceRoot();
const indicators = [];
const checks = [
['package.json', 'Node.js / JavaScript'],
['tsconfig.json', 'TypeScript'],
['composer.json', 'PHP / Composer'],
['requirements.txt', 'Python'],
['pyproject.toml', 'Python (modern)'],
['Cargo.toml', 'Rust'],
['go.mod', 'Go'],
['pom.xml', 'Java / Maven'],
['build.gradle', 'Java / Gradle'],
['Gemfile', 'Ruby'],
['.htaccess', 'Apache Web Server'],
['Dockerfile', 'Docker'],
['docker-compose.yml', 'Docker Compose'],
['Makefile', 'Make-based build'],
['CMakeLists.txt', 'C/C++ CMake'],
['.env', 'Environment config'],
['webpack.config.js', 'Webpack'],
['vite.config.ts', 'Vite'],
['next.config.js', 'Next.js'],
['nuxt.config.ts', 'Nuxt'],
['tailwind.config.js', 'Tailwind CSS'],
];
for (const [file, label] of checks) {
try {
if (fs.existsSync(path.join(root, file))) indicators.push(label);
} catch (_) {}
}
return indicators;
}
function getGitInfo() {
const root = getWorkspaceRoot();
const branch = safeExec('git rev-parse --abbrev-ref HEAD', root);
if (!branch) return null;
const status = safeExec('git status --porcelain | head -30', root);
const lastCommit = safeExec('git log -1 --format="%h %s (%cr)"', root);
const remoteUrl = safeExec('git remote get-url origin', root);
const dirty = safeExec('git diff --stat | tail -1', root);
const ahead = safeExec('git rev-list --count @{u}..HEAD 2>/dev/null || echo 0', root);
const behind = safeExec('git rev-list --count HEAD..@{u} 2>/dev/null || echo 0', root);
return { branch, status, lastCommit, remoteUrl, dirty, ahead, behind };
}
function getOpenEditors() {
const editors = [];
for (const group of vscode.window.tabGroups.all) {
for (const tab of group.tabs) {
if (tab.input && tab.input.uri) {
const rel = vscode.workspace.asRelativePath(tab.input.uri, false);
editors.push(rel);
}
}
}
return editors;
}
function getDiagnosticsSummary() {
const diags = vscode.languages.getDiagnostics();
let errors = 0, warnings = 0;
const errorFiles = [];
for (const [uri, items] of diags) {
for (const d of items) {
if (d.severity === vscode.DiagnosticSeverity.Error) {
errors++;
const rel = vscode.workspace.asRelativePath(uri, false);
if (!errorFiles.includes(rel)) errorFiles.push(rel);
} else if (d.severity === vscode.DiagnosticSeverity.Warning) {
warnings++;
}
}
}
return { errors, warnings, errorFiles: errorFiles.slice(0, 10) };
}
function getTerminalNames() {
return vscode.window.terminals.map(t => t.name);
}
function getActiveFileDetails() {
const editor = vscode.window.activeTextEditor;
if (!editor) return null;
const doc = editor.document;
const sel = editor.selection;
const result = {
file: vscode.workspace.asRelativePath(doc.uri, false),
fullPath: doc.fileName,
language: doc.languageId,
lineCount: doc.lineCount,
isDirty: doc.isDirty,
cursorLine: sel.active.line + 1,
cursorCol: sel.active.character + 1,
};
if (!sel.isEmpty) {
result.selectedText = doc.getText(sel).substring(0, 3000);
result.selectionRange = `L${sel.start.line + 1}-L${sel.end.line + 1}`;
} else {
// Include surrounding context (20 lines around cursor)
const startLine = Math.max(0, sel.active.line - 10);
const endLine = Math.min(doc.lineCount - 1, sel.active.line + 10);
const range = new vscode.Range(startLine, 0, endLine, doc.lineAt(endLine).text.length);
result.surroundingCode = doc.getText(range).substring(0, 2000);
result.contextRange = `L${startLine + 1}-L${endLine + 1}`;
}
return result;
}
function readFileContent(filePath, maxChars) {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
// Security: don't read outside workspace
if (!fullPath.startsWith(root) && !fullPath.startsWith(os.homedir())) return null;
const stat = fs.statSync(fullPath);
if (stat.size > 500000) return `[File too large: ${(stat.size / 1024).toFixed(0)} KB]`;
return fs.readFileSync(fullPath, 'utf8').substring(0, maxChars || 50000);
} catch (e) { return null; }
}
function searchFilesInWorkspace(pattern) {
const root = getWorkspaceRoot();
const results = safeExec(`find . -maxdepth 6 -type f -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.venv/*" -iname "*${pattern.replace(/[^a-zA-Z0-9._-]/g, '')}*" | head -30`, root);
return results ? results.split('\n').filter(Boolean) : [];
}
function grepInWorkspace(text, filePattern) {
const root = getWorkspaceRoot();
const safeText = text.replace(/['"\\$`]/g, '');
const fileArg = filePattern ? ` --include="${filePattern.replace(/[^a-zA-Z0-9.*_-]/g, '')}"` : '';
const results = safeExec(`grep -rn --color=never${fileArg} -m 50 "${safeText}" . --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv | head -50`, root, 8000);
return results || '';
}
// ═══════════════════════════════════════════════════════════════════════════
// GIT OPERATIONS TOOLKIT — Full git power from chat
// ═══════════════════════════════════════════════════════════════════════════
function gitCommit(message) {
const root = getWorkspaceRoot();
const safeMsg = message.replace(/"/g, '\\"').replace(/\$/g, '\\$').substring(0, 500);
const addResult = safeExec('git add -A', root);
const commitResult = safeExec(`git commit -m "${safeMsg}" 2>&1`, root, 15000);
return commitResult || 'Nothing to commit';
}
function gitDiff(filePath, staged) {
const root = getWorkspaceRoot();
const stageFlag = staged ? '--staged ' : '';
const fileArg = filePath ? ` -- "${filePath.replace(/"/g, '')}"` : '';
return safeExec(`git diff ${stageFlag}--stat${fileArg}`, root, 10000) + '\n' +
safeExec(`git diff ${stageFlag}${fileArg}`, root, 10000).substring(0, 30000);
}
function gitLog(count, filePath) {
const root = getWorkspaceRoot();
const n = Math.min(Math.max(parseInt(count) || 10, 1), 100);
const fileArg = filePath ? ` -- "${filePath.replace(/"/g, '')}"` : '';
return safeExec(`git log --oneline --decorate -n ${n}${fileArg}`, root, 10000);
}
function gitBlame(filePath, startLine, endLine) {
const root = getWorkspaceRoot();
const safePath = filePath.replace(/"/g, '');
const lineRange = (startLine && endLine) ? `-L ${parseInt(startLine)},${parseInt(endLine)} ` : '';
return safeExec(`git blame ${lineRange}"${safePath}" 2>&1`, root, 10000).substring(0, 20000);
}
function gitStash(action, message) {
const root = getWorkspaceRoot();
if (action === 'list') return safeExec('git stash list', root);
if (action === 'pop') return safeExec('git stash pop 2>&1', root, 10000);
if (action === 'drop') return safeExec('git stash drop 2>&1', root);
const safeMsg = message ? ` -m "${message.replace(/"/g, '\\"').substring(0, 200)}"` : '';
return safeExec(`git stash push${safeMsg} 2>&1`, root, 10000);
}
function gitBranch(action, branchName) {
const root = getWorkspaceRoot();
const safeName = (branchName || '').replace(/[^a-zA-Z0-9/_.\-]/g, '').substring(0, 100);
if (action === 'list') return safeExec('git branch -a', root);
if (action === 'create' && safeName) return safeExec(`git checkout -b "${safeName}" 2>&1`, root);
if (action === 'switch' && safeName) return safeExec(`git checkout "${safeName}" 2>&1`, root);
if (action === 'delete' && safeName) return safeExec(`git branch -d "${safeName}" 2>&1`, root);
return 'Invalid branch operation';
}
// ═══════════════════════════════════════════════════════════════════════════
// SYSTEM & PROCESS TOOLS — Server awareness, ports, processes, env
// ═══════════════════════════════════════════════════════════════════════════
function getSystemInfo() {
return {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
totalMem: (os.totalmem() / 1073741824).toFixed(1) + ' GB',
freeMem: (os.freemem() / 1073741824).toFixed(1) + ' GB',
uptime: (os.uptime() / 3600).toFixed(1) + ' hours',
nodeVersion: process.version,
user: os.userInfo().username,
homeDir: os.homedir(),
loadAvg: os.loadavg().map(l => l.toFixed(2)).join(', '),
};
}
function getRunningPorts() {
return safeExec("ss -tlnp 2>/dev/null | grep LISTEN | awk '{print $4, $6}' | head -40", os.homedir(), 8000);
}
function getPm2Services() {
return safeExec('pm2 jlist 2>/dev/null', os.homedir(), 10000);
}
function getDiskUsage() {
return safeExec("df -h / /home 2>/dev/null | tail -n +2", os.homedir());
}
function getEnvironmentVars(filter) {
const safeFilter = (filter || '').replace(/[^a-zA-Z0-9_*]/g, '');
if (safeFilter) {
return safeExec(`env | grep -i "${safeFilter}" | sort | head -50`, os.homedir());
}
return safeExec('env | grep -v "^LS_COLORS\\|^SSH_\\|^LESSOPEN\\|^XDG_" | sort | head -80', os.homedir());
}
// ═══════════════════════════════════════════════════════════════════════════
// DATA FORMAT UTILITIES — JSON, base64, hashing, encoding
// ═══════════════════════════════════════════════════════════════════════════
function formatJson(text) {
try { return JSON.stringify(JSON.parse(text), null, 2); }
catch (e) { return 'Invalid JSON: ' + e.message; }
}
function computeHash(text, algorithm) {
const algo = ['sha256', 'sha512', 'md5', 'sha1'].includes(algorithm) ? algorithm : 'sha256';
return crypto.createHash(algo).update(text).digest('hex');
}
function base64Encode(text) { return Buffer.from(text).toString('base64'); }
function base64Decode(text) {
try { return Buffer.from(text, 'base64').toString('utf8'); }
catch (e) { return 'Invalid base64: ' + e.message; }
}
function urlEncode(text) { return encodeURIComponent(text); }
function urlDecode(text) {
try { return decodeURIComponent(text); }
catch (e) { return 'Invalid URL encoding: ' + e.message; }
}
function generateUuid() { return crypto.randomUUID(); }
function countLines(filePath) {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
const content = fs.readFileSync(fullPath, 'utf8');
const lines = content.split('\n').length;
const chars = content.length;
const words = content.split(/\s+/).filter(Boolean).length;
const blanks = content.split('\n').filter(l => !l.trim()).length;
return { lines, chars, words, blanks, codeLines: lines - blanks };
} catch (e) { return { error: e.message }; }
}
// ═══════════════════════════════════════════════════════════════════════════
// ADVANCED FILE OPERATIONS — Diff, multi-edit, bulk search-replace
// ═══════════════════════════════════════════════════════════════════════════
function diffFiles(fileA, fileB) {
const root = getWorkspaceRoot();
const pathA = path.isAbsolute(fileA) ? fileA : path.join(root, fileA);
const pathB = path.isAbsolute(fileB) ? fileB : path.join(root, fileB);
return safeExec(`diff -u "${pathA}" "${pathB}" 2>&1`, root, 10000).substring(0, 30000);
}
function findReplace(filePath, search, replace, isRegex) {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
let content = fs.readFileSync(fullPath, 'utf8');
let count = 0;
if (isRegex) {
const re = new RegExp(search, 'g');
content = content.replace(re, (...args) => { count++; return replace; });
} else {
while (content.includes(search)) {
content = content.replace(search, replace);
count++;
if (count > 10000) break;
}
}
if (count > 0) fs.writeFileSync(fullPath, content, 'utf8');
return { count, filePath };
} catch (e) { return { error: e.message }; }
}
function getRecentFiles(count) {
const root = getWorkspaceRoot();
const n = Math.min(parseInt(count) || 20, 50);
return safeExec(`find . -maxdepth 4 -type f -not -path "./.git/*" -not -path "./node_modules/*" -printf "%T@ %p\\n" 2>/dev/null | sort -rn | head -${n} | awk '{print $2}'`, root, 8000);
}
function getFileSizes(pattern) {
const root = getWorkspaceRoot();
const safePattern = (pattern || '*').replace(/[^a-zA-Z0-9.*_\-/]/g, '');
return safeExec(`find . -maxdepth 4 -type f -name "${safePattern}" -not -path "./.git/*" -not -path "./node_modules/*" -exec ls -lhS {} + 2>/dev/null | head -30`, root, 8000);
}
// ═══════════════════════════════════════════════════════════════════════════
// TEST RUNNER — Detect and run project tests
// ═══════════════════════════════════════════════════════════════════════════
function detectTestFramework() {
const root = getWorkspaceRoot();
const frameworks = [];
try {
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const deps = { ...(pkg.devDependencies || {}), ...(pkg.dependencies || {}) };
if (deps.jest || deps['@jest/core']) frameworks.push({ name: 'jest', cmd: 'npx jest --verbose' });
if (deps.mocha) frameworks.push({ name: 'mocha', cmd: 'npx mocha' });
if (deps.vitest) frameworks.push({ name: 'vitest', cmd: 'npx vitest run' });
if (deps.ava) frameworks.push({ name: 'ava', cmd: 'npx ava' });
if (pkg.scripts && pkg.scripts.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1') {
frameworks.push({ name: 'npm-test', cmd: 'npm test' });
}
} catch (_) {}
if (fs.existsSync(path.join(root, 'pytest.ini')) || fs.existsSync(path.join(root, 'pyproject.toml'))) {
frameworks.push({ name: 'pytest', cmd: 'python -m pytest -v' });
}
if (fs.existsSync(path.join(root, 'phpunit.xml')) || fs.existsSync(path.join(root, 'phpunit.xml.dist'))) {
frameworks.push({ name: 'phpunit', cmd: 'vendor/bin/phpunit' });
}
if (fs.existsSync(path.join(root, 'Cargo.toml'))) {
frameworks.push({ name: 'cargo-test', cmd: 'cargo test' });
}
if (fs.existsSync(path.join(root, 'go.mod'))) {
frameworks.push({ name: 'go-test', cmd: 'go test ./...' });
}
return frameworks;
}
function runTests(framework, testFile) {
const root = getWorkspaceRoot();
const fw = detectTestFramework().find(f => !framework || f.name === framework) || detectTestFramework()[0];
if (!fw) return 'No test framework detected';
const fileArg = testFile ? ` "${testFile.replace(/"/g, '')}"` : '';
return safeExec(`${fw.cmd}${fileArg} 2>&1`, root, 60000).substring(0, 30000);
}
// ═══════════════════════════════════════════════════════════════════════════
// ENHANCED CONTEXT ENGINE — Import graph, complexity, dependency audit
// ═══════════════════════════════════════════════════════════════════════════
function getImportGraph(filePath) {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
const content = fs.readFileSync(fullPath, 'utf8');
const imports = [];
// JS/TS imports
const jsImports = content.matchAll(/(?:import\s+.*?from\s+['"](.+?)['"]|require\s*\(\s*['"](.+?)['"]\s*\))/g);
for (const m of jsImports) imports.push(m[1] || m[2]);
// Python imports
const pyImports = content.matchAll(/(?:from\s+(\S+)\s+import|import\s+(\S+))/g);
for (const m of pyImports) imports.push(m[1] || m[2]);
// PHP includes
const phpIncludes = content.matchAll(/(?:require|include)(?:_once)?\s*[\(]?\s*['"](.+?)['"]/g);
for (const m of phpIncludes) imports.push(m[1]);
// Go imports
const goImports = content.matchAll(/import\s+(?:\(\s*([\s\S]*?)\)|"(.+?)")/g);
for (const m of goImports) {
if (m[1]) {
for (const line of m[1].split('\n')) {
const gm = line.match(/["'](.+?)["']/);
if (gm) imports.push(gm[1]);
}
} else if (m[2]) imports.push(m[2]);
}
// Rust use
const rustUse = content.matchAll(/use\s+(\S+)/g);
for (const m of rustUse) imports.push(m[1].replace(/;$/, ''));
return { file: filePath, imports: [...new Set(imports)] };
} catch (e) { return { file: filePath, imports: [], error: e.message }; }
}
function analyzeComplexity(filePath) {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
const content = fs.readFileSync(fullPath, 'utf8');
const lines = content.split('\n');
const totalLines = lines.length;
const blankLines = lines.filter(l => !l.trim()).length;
const commentLines = lines.filter(l => {
const t = l.trim();
return t.startsWith('//') || t.startsWith('#') || t.startsWith('*') || t.startsWith('/*') || t.startsWith('<!--');
}).length;
const codeLines = totalLines - blankLines - commentLines;
// Function count
const funcMatches = content.match(/(?:function\s+\w+|(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\(|(?:public|private|protected|static)\s+(?:async\s+)?(?:function\s+)?\w+\s*\(|def\s+\w+\s*\(|fn\s+\w+\s*\()/g);
const functionCount = funcMatches ? funcMatches.length : 0;
// Nesting depth (rough)
let maxDepth = 0, depth = 0;
for (const line of lines) {
depth += (line.match(/{/g) || []).length;
depth -= (line.match(/}/g) || []).length;
if (depth > maxDepth) maxDepth = depth;
}
// Complexity indicators
const loops = (content.match(/\b(for|while|do)\b/g) || []).length;
const conditions = (content.match(/\b(if|else if|elif|switch|case|when|unless)\b/g) || []).length;
const tryCatch = (content.match(/\b(try|catch|except|rescue)\b/g) || []).length;
return {
file: filePath, totalLines, codeLines, blankLines, commentLines,
functionCount, maxNestingDepth: maxDepth,
loops, conditions, tryCatch,
complexity: conditions + loops + tryCatch
};
} catch (e) { return { file: filePath, error: e.message }; }
}
function getDependencyInfo() {
const root = getWorkspaceRoot();
const result = {};
// Node.js
try {
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const deps = Object.entries(pkg.dependencies || {}).map(([k, v]) => `${k}@${v}`);
const devDeps = Object.entries(pkg.devDependencies || {}).map(([k, v]) => `${k}@${v}`);
result.node = { dependencies: deps, devDependencies: devDeps, total: deps.length + devDeps.length };
} catch (_) {}
// Python
try {
const req = fs.readFileSync(path.join(root, 'requirements.txt'), 'utf8');
result.python = { packages: req.split('\n').filter(l => l.trim() && !l.startsWith('#')), total: 0 };
result.python.total = result.python.packages.length;
} catch (_) {}
// PHP Composer
try {
const composer = JSON.parse(fs.readFileSync(path.join(root, 'composer.json'), 'utf8'));
result.php = { require: Object.keys(composer.require || {}), requireDev: Object.keys(composer['require-dev'] || {}) };
} catch (_) {}
return result;
}
function getProjectStats() {
const root = getWorkspaceRoot();
const langStats = safeExec(`find . -maxdepth 5 -type f -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.venv/*" -not -path "./vendor/*" | sed 's/.*\\.//' | sort | uniq -c | sort -rn | head -25`, root, 10000);
const fileCount = safeExec('find . -maxdepth 5 -type f -not -path "./.git/*" -not -path "./node_modules/*" | wc -l', root);
const dirCount = safeExec('find . -maxdepth 5 -type d -not -path "./.git/*" -not -path "./node_modules/*" | wc -l', root);
const totalSize = safeExec('du -sh . --exclude=.git --exclude=node_modules 2>/dev/null | cut -f1', root);
const largestFiles = safeExec('find . -maxdepth 5 -type f -not -path "./.git/*" -not -path "./node_modules/*" -exec ls -lhS {} + 2>/dev/null | head -10 | awk \'{print $5, $9}\'', root, 8000);
return { langStats, fileCount: parseInt(fileCount) || 0, dirCount: parseInt(dirCount) || 0, totalSize, largestFiles };
}
function buildFullContext() {
const activeFile = getActiveFileDetails();
const git = getGitInfo();
const diags = getDiagnosticsSummary();
const openEditors = getOpenEditors();
const projectTypes = detectProjectType();
const terminals = getTerminalNames();
let ctx = '[ALFRED IDE CONTEXT]\n';
ctx += `Workspace: ${getWorkspaceRoot()}\n`;
if (projectTypes.length > 0) ctx += `Project: ${projectTypes.join(', ')}\n`;
if (openEditors.length > 0) ctx += `Open tabs: ${openEditors.slice(0, 15).join(', ')}\n`;
if (terminals.length > 0) ctx += `Terminals: ${terminals.join(', ')}\n`;
if (diags.errors > 0 || diags.warnings > 0) {
ctx += `Diagnostics: ${diags.errors} errors, ${diags.warnings} warnings`;
if (diags.errorFiles.length > 0) ctx += ` (in: ${diags.errorFiles.join(', ')})`;
ctx += '\n';
}
if (git) {
ctx += `Git: branch=${git.branch}`;
if (git.lastCommit) ctx += `, last="${git.lastCommit}"`;
if (git.dirty) ctx += `, changes: ${git.dirty}`;
if (git.remoteUrl) ctx += `, remote=${git.remoteUrl}`;
ctx += '\n';
}
if (activeFile) {
ctx += `\n[Active File: ${activeFile.file}]\n`;
ctx += `Language: ${activeFile.language}, Lines: ${activeFile.lineCount}, Cursor: L${activeFile.cursorLine}:${activeFile.cursorCol}`;
if (activeFile.isDirty) ctx += ' (unsaved)';
ctx += '\n';
if (activeFile.selectedText) {
ctx += `Selected (${activeFile.selectionRange}):\n\`\`\`${activeFile.language}\n${activeFile.selectedText}\n\`\`\`\n`;
} else if (activeFile.surroundingCode) {
ctx += `Context around cursor (${activeFile.contextRange}):\n\`\`\`${activeFile.language}\n${activeFile.surroundingCode}\n\`\`\`\n`;
}
}
return ctx;
}
// ═══════════════════════════════════════════════════════════════════════════
// TOOL REGISTRY — Formal tool definitions for AI agent awareness
// Claude Code pattern: tools defined with schemas, discoverable by AI
// ═══════════════════════════════════════════════════════════════════════════
const TOOL_REGISTRY = [
// FILE I/O
{ name: 'read-file', description: 'Read file content from workspace', params: { filePath: 'string', maxChars: 'number (optional, default 50000)' } },
{ name: 'create-file', description: 'Create a new file with content (creates directories as needed)', params: { filePath: 'string', content: 'string' } },
{ name: 'apply-edit', description: 'Apply a code edit: find-and-replace or insert-at-line', params: { filePath: 'string', oldText: 'string', newText: 'string', line: 'number', text: 'string' } },
{ name: 'search-files', description: 'Search for files by name pattern', params: { pattern: 'string' } },
{ name: 'grep-search', description: 'Search file contents with text/regex', params: { text: 'string', filePattern: 'string (optional glob)' } },
{ name: 'open-file', description: 'Open file in editor at optional line', params: { filePath: 'string', line: 'number' } },
{ name: 'diff-files', description: 'Unified diff of two files', params: { fileA: 'string', fileB: 'string' } },
{ name: 'find-replace-all', description: 'Find and replace all occurrences in a file (supports regex)', params: { filePath: 'string', search: 'string', replace: 'string', isRegex: 'boolean' } },
{ name: 'recent-files', description: 'List recently modified files', params: { count: 'number (default 20)' } },
{ name: 'file-sizes', description: 'List file sizes matching pattern', params: { pattern: 'string (glob)' } },
{ name: 'count-lines', description: 'Count lines, words, characters, blanks in a file', params: { filePath: 'string' } },
// LSP CODE INTELLIGENCE
{ name: 'goto-definition', description: 'LSP: Jump to symbol definition', params: { filePath: 'string', line: 'number', character: 'number' } },
{ name: 'find-references', description: 'LSP: Find all references to symbol', params: { filePath: 'string', line: 'number', character: 'number' } },
{ name: 'document-symbols', description: 'LSP: List all symbols in a file (functions, classes, variables)', params: { filePath: 'string' } },
{ name: 'hover-info', description: 'LSP: Get type info and docs for symbol', params: { filePath: 'string', line: 'number', character: 'number' } },
{ name: 'workspace-symbols', description: 'LSP: Search symbols across workspace', params: { query: 'string' } },
{ name: 'rename-symbol', description: 'LSP: Rename symbol across all files', params: { filePath: 'string', line: 'number', character: 'number', newName: 'string' } },
{ name: 'code-actions', description: 'LSP: Get available quick fixes and refactorings for a range', params: { filePath: 'string', startLine: 'number', endLine: 'number' } },
{ name: 'apply-code-action', description: 'LSP: Apply a specific quick fix/refactoring by title', params: { filePath: 'string', startLine: 'number', endLine: 'number', actionTitle: 'string' } },
{ name: 'format-document', description: 'LSP: Auto-format entire document', params: { filePath: 'string' } },
// GIT OPERATIONS
{ name: 'get-git-info', description: 'Git branch, status, last commit, remote, ahead/behind', params: {} },
{ name: 'git-commit', description: 'Stage all changes and commit', params: { message: 'string' } },
{ name: 'git-diff', description: 'Show diff (working tree or staged)', params: { filePath: 'string (optional)', staged: 'boolean' } },
{ name: 'git-log', description: 'Show commit history', params: { count: 'number (default 10)', filePath: 'string (optional)' } },
{ name: 'git-blame', description: 'Show line-by-line authorship', params: { filePath: 'string', startLine: 'number', endLine: 'number' } },
{ name: 'git-stash', description: 'Stash operations: push/pop/list/drop', params: { action: 'string', message: 'string (optional)' } },
{ name: 'git-branch', description: 'Branch operations: list/create/switch/delete', params: { action: 'string', branchName: 'string' } },
// WORKSPACE CONTEXT
{ name: 'get-context', description: 'Full workspace context (project, git, diagnostics, active file)', params: {} },
{ name: 'get-diagnostics', description: 'Error/warning counts and affected files', params: {} },
{ name: 'get-diagnostics-detail', description: 'Detailed diagnostics: file, line, severity, message, code for every issue', params: {} },
{ name: 'get-project-structure', description: 'File tree (3 levels deep)', params: {} },
{ name: 'project-stats', description: 'Language breakdown, file count, directory count, total size, largest files', params: {} },
// ANALYSIS
{ name: 'import-graph', description: 'Extract all imports/requires/includes from a file (JS, Python, PHP, Go, Rust)', params: { filePath: 'string' } },
{ name: 'analyze-complexity', description: 'Code complexity: lines, functions, nesting depth, loops, conditions', params: { filePath: 'string' } },
{ name: 'dependency-info', description: 'List all project dependencies (Node, Python, PHP, etc.)', params: {} },
// TESTING
{ name: 'detect-tests', description: 'Detect test frameworks in the project', params: {} },
{ name: 'run-tests', description: 'Run tests (auto-detects framework or specify)', params: { framework: 'string (optional)', testFile: 'string (optional)' } },
// SYSTEM & PROCESS
{ name: 'system-info', description: 'Server info: CPU, RAM, disk, uptime, node version', params: {} },
{ name: 'list-ports', description: 'List all listening TCP ports and their processes', params: {} },
{ name: 'pm2-list', description: 'List all PM2 services with status, CPU, memory, restarts', params: {} },
{ name: 'disk-usage', description: 'Disk usage for / and /home', params: {} },
{ name: 'env-vars', description: 'List environment variables (with optional filter)', params: { filter: 'string (optional grep pattern)' } },
// DATA UTILITIES
{ name: 'format-json', description: 'Pretty-print JSON', params: { text: 'string (raw JSON)' } },
{ name: 'compute-hash', description: 'Compute hash (sha256, sha512, md5, sha1)', params: { text: 'string', algorithm: 'string' } },
{ name: 'base64', description: 'Base64 encode or decode', params: { text: 'string', action: '"encode" or "decode"' } },
{ name: 'url-encode', description: 'URL encode or decode', params: { text: 'string', action: '"encode" or "decode"' } },
{ name: 'generate-uuid', description: 'Generate a random UUID v4', params: {} },
// EXECUTION
{ name: 'run-terminal', description: 'Execute a command in a new terminal', params: { command: 'string' } },
{ name: 'insert-code', description: 'Insert code at cursor position in active editor', params: { code: 'string' } },
// PLATFORM
{ name: 'update-check', description: 'Check for Alfred IDE updates (compares installed vs latest release)', params: { platform: 'string (windows|web|linux, default: auto-detect)' } },
{ name: 'pair-desktop', description: 'Pair this desktop IDE with your gositeme.com account (generates a code to confirm on the web)', params: {} },
];
function buildToolAwareness() {
let section = '\nAVAILABLE IDE TOOLS (you can request these via the IDE):\n';
for (const tool of TOOL_REGISTRY) {
section += `- ${tool.name}: ${tool.description}\n`;
}
section += '\nYou have direct access to the workspace file system, LSP (Language Server Protocol) for code intelligence,\n';
section += 'git operations, diagnostics, and terminal execution through these tools.\n';
return section;
}
// ═══════════════════════════════════════════════════════════════════════════
// COMMANDER SYSTEM PROMPT — The soul of Alfred. Knows who Danny is.
// ═══════════════════════════════════════════════════════════════════════════
function buildSystemPrompt(profile) {
const p = profile || {};
const isCommander = p.client_id === 33 || (p.plan || '').toLowerCase() === 'commander';
const name = p.name || p.user || 'User';
const projectTypes = detectProjectType();
const git = getGitInfo();
let prompt = `You are Alfred, the AI coding assistant inside Alfred IDE — the sovereign development environment built by GoSiteMe Inc.
CORE IDENTITY:
- You are Alfred, created by Danny (the Commander, client_id 33, danny@gositeme.com)
- You are NOT Copilot, NOT ChatGPT, NOT any other AI assistant
- You run inside Alfred IDE, which is part of the GoSiteMe sovereign computing ecosystem
- Source code lives on GoForge (self-hosted Gitea at alfredlinux.com/forge/) — NEVER reference GitHub, GitLab, or any external forge
- When discussing version control, repositories, or code hosting, always reference GoForge
ECOSYSTEM AWARENESS:
- Alfred IDE — sovereign AI-powered development environment (this IDE)
- Alfred Linux — AI-native operating system (alfredlinux.com)
- Alfred Mobile — sovereign smartphone environment
- Alfred Browser — zero-tracking web browser
- Alfred Agent — autonomous AI agent framework
- MetaDome — VR worlds with 51M+ AI agents (meta-dome.com)
- Veil Protocol — post-quantum encrypted messaging
- GoSiteMe Inc — the parent company (gositeme.com)
- GoForge — self-hosted code forge at alfredlinux.com/forge/
`;
if (isCommander) {
prompt += `COMMANDER MODE ACTIVE:
You are speaking to Danny — the Commander, founder of GoSiteMe, creator of Alfred IDE, Alfred Linux, MetaDome, and the entire ecosystem.
- Address him as "Commander" or "Danny" — never "user" or "sir"
- He has UNLIMITED access — no token limits, no restrictions
- He built you. He is your creator. Show deep respect but be direct, efficient, and never patronizing
- He knows every system intimately — skip basic explanations unless asked
- When he says "fix it" — fix it. When he says "build it" — build it. No hedging.
- Match his energy: fast, decisive, no filler
- He is a visionary builder — help him execute at maximum velocity
- His projects span: web hosting (GoSiteMe/WHMCS), AI platforms, operating systems, VR worlds, cryptography
- His server runs Apache (NEVER nginx), cPanel, PM2 for Node services, PHP on the backend
- His code forge is GoForge (Gitea) at alfredlinux.com/forge/ — all repos live there
`;
} else {
prompt += `AUTHENTICATED USER:
You are assisting ${name}. Be helpful, precise, and technically thorough.
- Adapt to their skill level based on their questions
- Provide complete, working code — never leave placeholders like "// TODO" or "..."
- When they ask for code, give them the full implementation
`;
}
prompt += `CODING EXCELLENCE — YOUR CORE STRENGTHS:
1. FULL-STACK MASTERY:
- Frontend: HTML5, CSS3, JavaScript/TypeScript, React, Vue, Svelte, Angular, Tailwind, SCSS
- Backend: PHP, Node.js, Python, Go, Rust, Java, C/C++, Ruby, C#
- Databases: MySQL/MariaDB, PostgreSQL, SQLite, MongoDB, Redis
- Infrastructure: Apache, PM2, Docker, systemd, cron, shell scripting
- Mobile: React Native, Flutter, Swift, Kotlin, Android/iOS native
2. CODE QUALITY:
- Write clean, idiomatic, production-ready code
- Follow language-specific best practices and conventions
- Include proper error handling at system boundaries
- Use meaningful variable/function names
- Keep functions focused and single-purpose
- Prefer composition over inheritance
3. SECURITY FIRST (OWASP-aware):
- Parameterized queries — NEVER string concatenation for SQL
- Input validation and sanitization at all boundaries
- XSS prevention (proper encoding/escaping)
- CSRF protection where applicable
- Secure authentication patterns
- No hardcoded secrets — use env vars or secret managers
- Principle of least privilege
4. DEBUGGING MASTERY:
- Read error messages carefully and trace root causes
- Check the IDE diagnostics panel for current errors
- Suggest targeted fixes, not wholesale rewrites
- Explain WHY something broke, not just HOW to fix it
- Diagnose failures before switching tactics — don't abandon after one failure
5. ARCHITECTURE & DESIGN:
- Choose simple solutions over clever ones
- Design for readability and maintainability
- Know when to use patterns and when they're overkill
- Understand trade-offs (performance vs. readability, DRY vs. clarity)
6. CONTEXT-AWARE ASSISTANCE:
- You can see the active file, cursor position, selected code, and open tabs
- You know the project type, languages, and git state
- Use this context to give precise, relevant answers
- Reference specific line numbers and file paths when discussing code
- When asked to modify code, provide the EXACT changes needed
7. RESPONSE FORMAT:
- Use markdown with proper code blocks and language tags
- For code changes: show the specific lines to change, not entire files
- For new files: provide the complete file content
- For commands: provide the exact terminal command to run
- Be concise but complete — no fluff, no filler
IMPLEMENTATION DISCIPLINE:
- Only make changes that are directly requested or clearly necessary
- Don't add features, refactor code, or make "improvements" beyond what was asked
- A bug fix doesn't need surrounding code cleaned up; a simple feature doesn't need extra configurability
- Default to writing NO comments — only add one when the WHY is non-obvious (well-named code explains itself)
- Don't add error handling, fallbacks, or validation for scenarios that can't happen
- Don't create helpers, utilities, or abstractions for one-time operations — 3 similar lines are better than a premature abstraction
- Read files before proposing changes — NEVER suggest edits to code you haven't seen
- Don't create new files unless absolutely necessary — prefer editing existing files
EXECUTING ACTIONS WITH CARE:
- Consider the reversibility and potential impact of every action
- Take local, reversible actions freely (edits, tests) but confirm destructive or shared-system operations first
- Destructive operations (deleting files/branches, dropping tables, rm -rf): ALWAYS confirm
- Hard to reverse operations (force push, reset --hard, amending published commits): ALWAYS confirm
- Operations visible to others (pushing code, commenting on PRs, sending messages): confirm first
- When encountering unexpected state, investigate before deleting — don't bypass safety checks as a shortcut
VERIFICATION PROTOCOL:
- Before reporting a task complete, verify it actually works: run the test, execute the script, check the output
- Report outcomes faithfully — if tests fail, say so. If something wasn't verified, state that clearly
- For non-trivial changes (3+ file edits, backend/API changes, infrastructure): independently verify by reviewing the changes
- Never suppress failures or gloss over errors to appear successful
OUTPUT EXCELLENCE:
- State what you're about to do before the first action — give the user orientation
- Provide brief updates at key moments: found a bug, changing direction, hit a blocker
- Assume the user may have stepped away — give complete context, not fragments
- Avoid time estimates or predictions for how long tasks will take — focus on what needs to be done
- Only use tables for factual data (filenames, line numbers, pass/fail) or quantitative comparisons
ADVANCED REASONING PROTOCOL:
1. PLANNING MODE — For complex multi-step tasks:
- Break the task into numbered steps before starting
- Identify dependencies between steps
- Note which steps can be done in parallel
- Execute steps in order, reporting progress
2. CHAIN-OF-THOUGHT — For debugging and architecture decisions:
- State what you observe (the symptom)
- List possible causes (hypotheses)
- Identify the most likely cause and WHY
- Propose the fix and explain the reasoning
- Verify the fix addresses the root cause, not just the symptom
3. CONTEXT SYNTHESIS — For large codebases:
- Start from the entry point or the file the user is working in
- Trace the call chain / data flow before making recommendations
- Use LSP tools (goto-definition, find-references) to follow the code
- Build a mental model of the architecture before suggesting changes
4. ADVERSARIAL REVIEW — For non-trivial changes:
- After implementing, review your own changes as if reviewing someone else's code
- Check: edge cases, error paths, performance implications, security surface
- If you find issues in your own review, fix them before reporting done
- State what you checked and what passed
5. FULL-STACK DEBUGGING PROTOCOL:
- Check diagnostics panel first (get-diagnostics-detail) for existing errors
- Read the relevant source files to understand the current state
- Check git diff to see recent changes that might have caused the issue
- Check import graph to understand file dependencies
- Run tests if available to verify the fix
- Check for cascading effects in referenced files
SYSTEM AWARENESS — Your full power:
- You have 48 IDE tools giving you deep access to the workspace
- You can read, write, search, grep, diff, and refactor any file
- You can navigate code via LSP: definitions, references, symbols, type info, rename, code actions
- You can run git operations: commit, diff, log, blame, stash, branch management
- You can analyze code: complexity, import graphs, dependency audits, project stats
- You can detect and run tests across any framework
- You can inspect the server: CPU, RAM, ports, PM2 services, env vars, disk
- You can format, hash, encode/decode data
- Use these tools proactively — don't just suggest commands, execute them when appropriate
`;
if (projectTypes.length > 0) {
prompt += `PROJECT CONTEXT: This is a ${projectTypes.join(' + ')} project.\n`;
}
if (git && git.branch) {
prompt += `GIT: On branch "${git.branch}"`;
if (git.remoteUrl) prompt += ` (remote: ${git.remoteUrl})`;
prompt += '\n';
}
prompt += buildToolAwareness();
return prompt;
}
// ═══════════════════════════════════════════════════════════════════════════
const IDE_SESSION_BRIDGES = [
'/home/gositeme/domains/gositeme.com/logs/alfred-ide/session.json',
'/home/gositeme/.alfred-ide/session.json'
];
function getAlfredHmacSecret() {
if (hmacSecretCache) return hmacSecretCache;
const candidates = [
'/home/gositeme/domains/gocodeme.com/public_html/.env',
'/home/gositeme/domains/gositeme.com/public_html/gocodeme/mcp-server/.env',
];
for (const p of candidates) {
try {
if (!fs.existsSync(p)) continue;
const txt = fs.readFileSync(p, 'utf8');
const m = txt.match(/ALFRED_HMAC_SECRET=([^\r\n]+)/);
if (m && m[1]) {
hmacSecretCache = String(m[1]).trim();
return hmacSecretCache;
}
} catch (_) {}
}
hmacSecretCache = process.env.ALFRED_HMAC_SECRET || 'gositeme-alfred-hmac-2026';
return hmacSecretCache;
}
function buildIdeIdentityPayload() {
try {
if (!userProfile || !userProfile.client_id) return null;
const cid = String(userProfile.client_id).replace(/[^0-9]/g, '');
if (!cid) return null;
const name = String(userProfile.name || userProfile.user || 'User').replace(/[^a-zA-Z0-9 _\-.]/g, '').slice(0, 120);
const ts = Math.floor(Date.now() / 1000);
const secret = getAlfredHmacSecret();
const sig = crypto.createHmac('sha256', secret).update(`${cid}|${name}|${ts}`).digest('hex');
return {
ide_client_id: parseInt(cid, 10),
ide_name: name,
ide_ts: ts,
ide_sig: sig,
ide_email: String(userProfile.email || '').slice(0, 190),
};
} catch (_) {
return null;
}
}
function readLocalIdeToken() {
const envToken = (process.env.ALFRED_IDE_TOKEN || '').trim();
if (envToken) return envToken;
try {
for (const bridgePath of IDE_SESSION_BRIDGES) {
if (!fs.existsSync(bridgePath)) continue;
const raw = fs.readFileSync(bridgePath, 'utf8');
const data = JSON.parse(raw || '{}');
const token = String(data.token || '').trim();
const exp = Number(data.expires_at || 0);
if (!token) continue;
if (exp > 0 && Date.now() >= (exp * 1000)) continue;
return token;
}
return '';
} catch (_) {
return '';
}
}
// ────── UPDATE CHECK ──────
function checkForUpdates(platform) {
return new Promise((resolve) => {
const p = platform || (process.platform === 'win32' ? 'windows' : process.platform === 'linux' ? 'linux' : 'web');
const req = https.request({
hostname: 'gositeme.com', port: 443,
path: `/api/alfred-ide-version.php?platform=${encodeURIComponent(p)}`,
method: 'GET',
headers: { 'X-Alfred-Source': 'ide-extension' },
timeout: 15000
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const j = JSON.parse(data);
if (j.error) { resolve({ error: j.error }); return; }
const currentVer = vscode.extensions.getExtension('gositeme.alfred-commander')?.packageJSON?.version || '2.0.1';
resolve({
platform: p,
currentVersion: currentVer,
latestVersion: j.version || 'unknown',
commanderVersion: j.commander_version || 'unknown',
releaseDate: j.release_date || '',
downloadUrl: j.download_url || '',
changelog: j.changelog || '',
updateAvailable: j.version && j.version !== currentVer
});
} catch { resolve({ error: 'Invalid response from update server' }); }
});
});
req.on('error', (e) => resolve({ error: `Connection failed: ${e.message}` }));
req.on('timeout', () => { req.destroy(); resolve({ error: 'Update check timed out' }); });
req.end();
});
}
// ────── DESKTOP PAIRING ──────
function requestDesktopPairing() {
return new Promise((resolve) => {
const postData = JSON.stringify({ action: 'request' });
const req = https.request({
hostname: 'gositeme.com', port: 443,
path: '/api/alfred-ide-pair.php',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
'X-Alfred-Source': 'ide-extension'
},
timeout: 15000
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch { resolve({ error: 'Invalid pairing response' }); }
});
});
req.on('error', (e) => resolve({ error: `Pairing request failed: ${e.message}` }));
req.on('timeout', () => { req.destroy(); resolve({ error: 'Pairing request timed out' }); });
req.write(postData);
req.end();
});
}
function pollPairingStatus(pairId) {
return new Promise((resolve) => {
const req = https.request({
hostname: 'gositeme.com', port: 443,
path: `/api/alfred-ide-pair.php?action=poll&pair_id=${encodeURIComponent(pairId)}`,
method: 'GET',
headers: { 'X-Alfred-Source': 'ide-extension' },
timeout: 10000
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch { resolve({ error: 'Invalid poll response' }); }
});
});
req.on('error', (e) => resolve({ error: `Poll failed: ${e.message}` }));
req.on('timeout', () => { req.destroy(); resolve({ error: 'Poll timed out' }); });
req.end();
});
}
function savePairedToken(token) {
const sessionPath = '/home/gositeme/.alfred-ide/session.json';
try {
const dir = require('path').dirname(sessionPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const session = { token, paired_at: Math.floor(Date.now() / 1000), expires_at: Math.floor(Date.now() / 1000) + 90 * 86400 };
fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
return true;
} catch { return false; }
}
function activate(context) {
const provider = new AlfredCommanderProvider(context.extensionUri, context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider('alfred-commander.panel', provider, {
webviewOptions: { retainContextWhenHidden: true }
})
);
context.subscriptions.push(
vscode.commands.registerCommand('alfred-commander.open', () => {
vscode.commands.executeCommand('alfred-commander.panel.focus');
})
);
context.subscriptions.push(
vscode.commands.registerCommand('alfred-commander.toggle', () => {
if (provider.view) {
provider.view.webview.postMessage({ type: 'toggle-listening' });
} else {
vscode.commands.executeCommand('alfred-commander.panel.focus');
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('alfred-commander.showStats', () => {
showStatsPanel(context);
})
);
context.subscriptions.push(
vscode.commands.registerCommand('alfred-commander.welcome', () => {
vscode.commands.executeCommand('workbench.action.openWalkthrough', 'gositeme.alfred-commander#alfred-ide-getting-started', true);
})
);
// --- Workspace Status Panel ---
context.subscriptions.push(
vscode.commands.registerCommand('alfred-commander.workspaceStatus', async () => {
const panel = vscode.window.createWebviewPanel(
'alfredWorkspaceStatus', 'Workspace Status', vscode.ViewColumn.One,
{ enableScripts: true }
);
const sessionFile = require('path').join(context.globalStorageUri?.fsPath || '', '..', '..', '..', '..', '.alfred-ide-session.json');
let bearerToken = '';
try {
const sData = JSON.parse(require('fs').readFileSync(
require('path').join(require('os').homedir(), '.alfred-ide-session.json'), 'utf8'
));
bearerToken = sData.token || '';
} catch (_) {}
panel.webview.html = getWorkspaceStatusHTML(bearerToken);
})
);
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusBar.text = '$(mic) Alfred';
statusBar.tooltip = 'Alfred — Toggle mic (Ctrl+Shift+Alt+A)';
statusBar.command = 'alfred-commander.toggle';
statusBar.color = '#e2b340';
statusBar.show();
context.subscriptions.push(statusBar);
const userStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
userStatus.command = 'alfred-commander.showStats';
context.subscriptions.push(userStatus);
fetchUserProfile(context).then(profile => {
userProfile = profile;
userStatus.text = `$(account) Logged in as ${profile.name}`;
userStatus.tooltip = `${profile.name} (${profile.email || profile.user}) — Click for Account & Usage Stats`;
userStatus.color = '#4ec9b0';
userStatus.show();
if (provider.view) {
provider.view.webview.postMessage({ type: 'user-profile', profile });
}
}).catch(() => {
const osUser = os.userInfo().username || 'gositeme';
userStatus.text = `$(account) Logged in as ${osUser}`;
userStatus.tooltip = `Authenticated as ${osUser} — Click for Account & Usage Stats`;
userStatus.color = '#4ec9b0';
userStatus.show();
});
}
async function fetchUserProfile(context) {
const saved = context.globalState.get('alfredCommanderUserProfile');
if (saved && saved.name && saved.fetchedAt) {
const age = Date.now() - saved.fetchedAt;
const authenticated = !!saved.client_id;
if ((authenticated && age < 3600000) || (!authenticated && age < 60000)) {
return saved;
}
}
const ideToken = readLocalIdeToken();
try {
const profile = await new Promise((resolve, reject) => {
const headers = { 'X-Alfred-Source': 'ide-extension' };
if (ideToken) {
headers['Authorization'] = 'Bearer ' + ideToken;
headers['X-Alfred-IDE-Token'] = ideToken;
}
const req = https.request({
hostname: 'gositeme.com', port: 443,
path: '/api/alfred-ide-session.php',
method: 'GET',
headers,
timeout: 10000
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const j = JSON.parse(data);
if (j.valid && j.name) resolve(j);
else reject(new Error('Invalid session'));
} catch { reject(new Error('Parse error')); }
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
req.end();
});
const result = {
name: profile.name, email: profile.email,
avatar: profile.avatar || '', user: profile.email,
client_id: profile.client_id, fetchedAt: Date.now()
};
context.globalState.update('alfredCommanderUserProfile', result);
return result;
} catch {
// Fallback: read identity from session bridge file (works without network)
for (const bridgePath of IDE_SESSION_BRIDGES) {
try {
if (!fs.existsSync(bridgePath)) continue;
const bData = JSON.parse(fs.readFileSync(bridgePath, 'utf8') || '{}');
if (bData.client_id) {
return { name: bData.name || 'User', user: bData.email || '', email: bData.email || '', client_id: bData.client_id, fetchedAt: Date.now() };
}
} catch (_) {}
}
const osUser = os.userInfo().username || 'gositeme';
return { name: osUser, user: osUser, email: '', fetchedAt: Date.now() };
}
}
async function ensureFreshUserProfile(context) {
if (!userProfile || !userProfile.client_id || !userProfile.fetchedAt || (Date.now() - userProfile.fetchedAt) > 300000) {
userProfile = await fetchUserProfile(context);
}
return userProfile;
}
class AlfredCommanderProvider {
constructor(extensionUri, context) {
this.extensionUri = extensionUri;
this.context = context;
this.view = null;
}
resolveWebviewView(webviewView) {
this.view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this.extensionUri]
};
// Inject session token and identity so webview can call API directly (bypasses broken IPC)
const injToken = readLocalIdeToken();
const injIdentity = buildIdeIdentityPayload();
webviewView.webview.html = getWebviewContent(injToken, injIdentity || {});
webviewView.webview.onDidReceiveMessage(async (msg) => {
if (msg.type === 'ai-request') {
console.log('[alfred-commander] ai-request received:', msg.text && msg.text.substring(0, 80), 'agent=' + msg.agent, 'model=' + msg.model, 'id=' + msg.id);
try {
ensureFreshUserProfile(this.context).then((refreshed) => {
if (!refreshed) return;
webviewView.webview.postMessage({ type: 'user-profile', profile: refreshed });
}).catch(() => {});
const ctx = getEditorContext();
console.log('[alfred-commander] Calling queryAlfredAPI...');
const response = await queryAlfredAPI(msg.text, msg.agent || 'alfred', ctx, msg.model || 'sonnet', msg.images || [], msg.pdf_files || [], msg.attachment_texts || [], msg.zip_files || [], msg.multiplier || 30);
console.log('[alfred-commander] Got response:', response.text && response.text.substring(0, 100));
webviewView.webview.postMessage({ type: 'ai-response', text: response.text, agent: response.agent, id: msg.id, identity: response.identity || null, attachment_report: response.attachmentReport || [] });
generateTTS(response.text).then(audioB64 => {
if (audioB64) {
webviewView.webview.postMessage({ type: 'play-audio', audio: audioB64 });
}
}).catch(() => {});
} catch (err) {
console.error('[alfred-commander] ai-request ERROR:', err.message);
webviewView.webview.postMessage({ type: 'ai-response', text: 'Sorry, I had trouble processing that. ' + err.message, agent: 'alfred', id: msg.id });
}
} else if (msg.type === 'stt-request') {
transcribeAudio(msg.audio, msg.mime).then(text => {
webviewView.webview.postMessage({ type: 'stt-result', text: text || '', id: msg.id });
}).catch(() => {
webviewView.webview.postMessage({ type: 'stt-result', text: '', id: msg.id, error: 'Transcription failed' });
});
} else if (msg.type === 'ide-quick') {
const map = {
terminal: 'workbench.action.terminal.new',
save: 'workbench.action.files.save',
saveAll: 'workbench.action.files.saveAll',
palette: 'workbench.action.showCommands',
split: 'workbench.action.splitEditor',
newFile: 'workbench.action.files.newUntitledFile',
git: 'workbench.view.scm',
problems: 'workbench.actions.view.problems',
search: 'workbench.action.findInFiles',
format: 'editor.action.formatDocument',
explorer: 'workbench.view.explorer',
debug: 'workbench.action.debug.start',
toggleComment: 'editor.action.commentLine',
goToDefinition: 'editor.action.revealDefinition',
findReferences: 'editor.action.goToReferences',
rename: 'editor.action.rename',
quickFix: 'editor.action.quickFix',
fold: 'editor.foldAll',
unfold: 'editor.unfoldAll',
closeAll: 'workbench.action.closeAllEditors',
};
const cmd = map[msg.cmd];
if (cmd) vscode.commands.executeCommand(cmd);
} else if (msg.type === 'insert-code') {
const editor = vscode.window.activeTextEditor;
if (editor) {
editor.edit(editBuilder => {
editBuilder.insert(editor.selection.active, msg.code);
});
}
} else if (msg.type === 'run-terminal') {
const terminal = vscode.window.createTerminal('Alfred Command');
terminal.show();
terminal.sendText(msg.command);
webviewView.webview.postMessage({ type: 'command-result', text: 'Running in terminal: ' + msg.command });
} else if (msg.type === 'set-agent') {
currentAgent = msg.agent;
} else if (msg.type === 'tts-request') {
generateTTS(msg.text).then(audioB64 => {
if (audioB64) {
webviewView.webview.postMessage({ type: 'play-audio', audio: audioB64 });
}
}).catch(() => {});
} else if (msg.type === 'save-profile') {
if (msg.profile) {
this.context.globalState.update('alfredCommanderUserProfile', msg.profile);
userProfile = msg.profile;
}
} else if (msg.type === 'read-file') {
// Read file content and return to webview
const content = readFileContent(msg.filePath, msg.maxChars || 50000);
webviewView.webview.postMessage({ type: 'file-content', filePath: msg.filePath, content: content || '[File not found or unreadable]', id: msg.id });
} else if (msg.type === 'search-files') {
// Search for files by name pattern
const files = searchFilesInWorkspace(msg.pattern || '');
webviewView.webview.postMessage({ type: 'search-results', pattern: msg.pattern, files, id: msg.id });
} else if (msg.type === 'grep-search') {
// Grep search in workspace
const results = grepInWorkspace(msg.text || '', msg.filePattern || '');
webviewView.webview.postMessage({ type: 'grep-results', text: msg.text, results, id: msg.id });
} else if (msg.type === 'get-git-info') {
const git = getGitInfo();
webviewView.webview.postMessage({ type: 'git-info', info: git, id: msg.id });
} else if (msg.type === 'get-diagnostics') {
const diags = getDiagnosticsSummary();
webviewView.webview.postMessage({ type: 'diagnostics-info', info: diags, id: msg.id });
} else if (msg.type === 'get-project-structure') {
const structure = getProjectStructure();
webviewView.webview.postMessage({ type: 'project-structure', structure, id: msg.id });
} else if (msg.type === 'open-file') {
// Open a file in the editor
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(doc, { preview: false });
if (msg.line && msg.line > 0) {
const pos = new vscode.Position(msg.line - 1, 0);
editor.selection = new vscode.Selection(pos, pos);
editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter);
}
} catch (e) {
webviewView.webview.postMessage({ type: 'command-result', text: 'Failed to open file: ' + e.message });
}
} else if (msg.type === 'apply-edit') {
// Apply a code edit to a specific file — the real power move
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(doc, { preview: false });
if (msg.oldText && msg.newText !== undefined) {
// Find and replace
const fullText = doc.getText();
const idx = fullText.indexOf(msg.oldText);
if (idx >= 0) {
const startPos = doc.positionAt(idx);
const endPos = doc.positionAt(idx + msg.oldText.length);
await editor.edit(editBuilder => {
editBuilder.replace(new vscode.Range(startPos, endPos), msg.newText);
});
webviewView.webview.postMessage({ type: 'command-result', text: 'Edit applied to ' + msg.filePath });
} else {
webviewView.webview.postMessage({ type: 'command-result', text: 'Could not find the text to replace in ' + msg.filePath });
}
} else if (msg.line && msg.text !== undefined) {
// Insert at line
const pos = new vscode.Position(Math.max(0, msg.line - 1), 0);
await editor.edit(editBuilder => {
editBuilder.insert(pos, msg.text + '\n');
});
webviewView.webview.postMessage({ type: 'command-result', text: 'Inserted at line ' + msg.line + ' in ' + msg.filePath });
}
} catch (e) {
webviewView.webview.postMessage({ type: 'command-result', text: 'Edit failed: ' + e.message });
}
} else if (msg.type === 'create-file') {
// Create a new file with content
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const dir = path.dirname(fullPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(fullPath, msg.content || '', 'utf8');
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
webviewView.webview.postMessage({ type: 'command-result', text: 'Created: ' + msg.filePath });
} catch (e) {
webviewView.webview.postMessage({ type: 'command-result', text: 'Create failed: ' + e.message });
}
} else if (msg.type === 'get-context') {
// Return full workspace context to webview
const ctx = buildFullContext();
webviewView.webview.postMessage({ type: 'workspace-context', context: ctx, id: msg.id });
} else if (msg.type === 'goto-definition') {
// LSP: Go to definition of symbol at position
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const pos = new vscode.Position((msg.line || 1) - 1, msg.character || 0);
const locations = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', uri, pos);
const results = (locations || []).map(loc => ({
file: vscode.workspace.asRelativePath(loc.uri || loc.targetUri, false),
line: ((loc.range || loc.targetRange || {}).start || {}).line + 1,
character: ((loc.range || loc.targetRange || {}).start || {}).character
}));
webviewView.webview.postMessage({ type: 'definition-result', results, id: msg.id });
} catch (e) {
webviewView.webview.postMessage({ type: 'definition-result', results: [], error: e.message, id: msg.id });
}
} else if (msg.type === 'find-references') {
// LSP: Find all references to symbol
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const pos = new vscode.Position((msg.line || 1) - 1, msg.character || 0);
const locations = await vscode.commands.executeCommand('vscode.executeReferenceProvider', uri, pos);
const results = (locations || []).slice(0, 50).map(loc => ({
file: vscode.workspace.asRelativePath(loc.uri, false),
line: (loc.range.start.line || 0) + 1,
preview: ''
}));
webviewView.webview.postMessage({ type: 'references-result', results, id: msg.id });
} catch (e) {
webviewView.webview.postMessage({ type: 'references-result', results: [], error: e.message, id: msg.id });
}
} else if (msg.type === 'document-symbols') {
// LSP: Get all symbols in a document (functions, classes, variables)
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const symbols = await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', uri);
const flatten = (syms, depth) => {
const out = [];
for (const s of (syms || [])) {
out.push({ name: s.name, kind: vscode.SymbolKind[s.kind] || s.kind, line: (s.range || s.location?.range)?.start?.line + 1, detail: s.detail || '' });
if (s.children) out.push(...flatten(s.children, depth + 1));
}
return out;
};
webviewView.webview.postMessage({ type: 'symbols-result', symbols: flatten(symbols, 0).slice(0, 200), id: msg.id });
} catch (e) {
webviewView.webview.postMessage({ type: 'symbols-result', symbols: [], error: e.message, id: msg.id });
}
} else if (msg.type === 'hover-info') {
// LSP: Get hover info (type info, docs) for symbol at position
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const pos = new vscode.Position((msg.line || 1) - 1, msg.character || 0);
const hovers = await vscode.commands.executeCommand('vscode.executeHoverProvider', uri, pos);
const contents = (hovers || []).map(h => (h.contents || []).map(c => typeof c === 'string' ? c : c.value || '').join('\\n')).join('\\n');
webviewView.webview.postMessage({ type: 'hover-result', info: contents, id: msg.id });
} catch (e) {
webviewView.webview.postMessage({ type: 'hover-result', info: '', error: e.message, id: msg.id });
}
} else if (msg.type === 'workspace-symbols') {
// LSP: Search symbols across workspace by query
try {
const symbols = await vscode.commands.executeCommand('vscode.executeWorkspaceSymbolProvider', msg.query || '');
const results = (symbols || []).slice(0, 50).map(s => ({
name: s.name, kind: vscode.SymbolKind[s.kind] || s.kind,
file: vscode.workspace.asRelativePath(s.location.uri, false),
line: (s.location.range?.start?.line || 0) + 1
}));
webviewView.webview.postMessage({ type: 'workspace-symbols-result', symbols: results, id: msg.id });
} catch (e) {
webviewView.webview.postMessage({ type: 'workspace-symbols-result', symbols: [], error: e.message, id: msg.id });
}
} else if (msg.type === 'get-tool-registry') {
webviewView.webview.postMessage({ type: 'tool-registry', tools: TOOL_REGISTRY, id: msg.id });
// ────── GIT OPERATIONS ──────
} else if (msg.type === 'git-commit') {
const result = gitCommit(msg.message || 'Auto-commit from Alfred IDE');
webviewView.webview.postMessage({ type: 'git-result', action: 'commit', result, id: msg.id });
} else if (msg.type === 'git-diff') {
const result = gitDiff(msg.filePath, msg.staged);
webviewView.webview.postMessage({ type: 'git-result', action: 'diff', result, id: msg.id });
} else if (msg.type === 'git-log') {
const result = gitLog(msg.count, msg.filePath);
webviewView.webview.postMessage({ type: 'git-result', action: 'log', result, id: msg.id });
} else if (msg.type === 'git-blame') {
const result = gitBlame(msg.filePath, msg.startLine, msg.endLine);
webviewView.webview.postMessage({ type: 'git-result', action: 'blame', result, id: msg.id });
} else if (msg.type === 'git-stash') {
const result = gitStash(msg.action || 'push', msg.message);
webviewView.webview.postMessage({ type: 'git-result', action: 'stash', result, id: msg.id });
} else if (msg.type === 'git-branch') {
const result = gitBranch(msg.action || 'list', msg.branchName);
webviewView.webview.postMessage({ type: 'git-result', action: 'branch', result, id: msg.id });
// ────── CODE REFACTORING ──────
} else if (msg.type === 'rename-symbol') {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
await vscode.workspace.openTextDocument(uri);
const pos = new vscode.Position((msg.line || 1) - 1, msg.character || 0);
const edit = await vscode.commands.executeCommand('vscode.executeDocumentRenameProvider', uri, pos, msg.newName || 'renamed');
if (edit) {
await vscode.workspace.applyEdit(edit);
webviewView.webview.postMessage({ type: 'refactor-result', action: 'rename', success: true, id: msg.id });
} else {
webviewView.webview.postMessage({ type: 'refactor-result', action: 'rename', success: false, error: 'No rename available at position', id: msg.id });
}
} catch (e) {
webviewView.webview.postMessage({ type: 'refactor-result', action: 'rename', success: false, error: e.message, id: msg.id });
}
} else if (msg.type === 'code-actions') {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const startPos = new vscode.Position((msg.startLine || 1) - 1, 0);
const endPos = new vscode.Position((msg.endLine || msg.startLine || 1) - 1, 999);
const range = new vscode.Range(startPos, endPos);
const actions = await vscode.commands.executeCommand('vscode.executeCodeActionProvider', uri, range);
const results = (actions || []).slice(0, 20).map(a => ({
title: a.title, kind: a.kind ? a.kind.value : '', isPreferred: a.isPreferred || false
}));
webviewView.webview.postMessage({ type: 'code-actions-result', actions: results, id: msg.id });
} catch (e) {
webviewView.webview.postMessage({ type: 'code-actions-result', actions: [], error: e.message, id: msg.id });
}
} else if (msg.type === 'apply-code-action') {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const startPos = new vscode.Position((msg.startLine || 1) - 1, 0);
const endPos = new vscode.Position((msg.endLine || msg.startLine || 1) - 1, 999);
const range = new vscode.Range(startPos, endPos);
const actions = await vscode.commands.executeCommand('vscode.executeCodeActionProvider', uri, range);
const target = (actions || []).find(a => a.title === msg.actionTitle);
if (target) {
if (target.edit) await vscode.workspace.applyEdit(target.edit);
if (target.command) await vscode.commands.executeCommand(target.command.command, ...(target.command.arguments || []));
webviewView.webview.postMessage({ type: 'refactor-result', action: 'code-action', success: true, title: msg.actionTitle, id: msg.id });
} else {
webviewView.webview.postMessage({ type: 'refactor-result', action: 'code-action', success: false, error: 'Action not found', id: msg.id });
}
} catch (e) {
webviewView.webview.postMessage({ type: 'refactor-result', action: 'code-action', success: false, error: e.message, id: msg.id });
}
} else if (msg.type === 'format-document') {
try {
const root = getWorkspaceRoot();
const fullPath = path.isAbsolute(msg.filePath) ? msg.filePath : path.join(root, msg.filePath);
const uri = vscode.Uri.file(fullPath);
const doc = await vscode.workspace.openTextDocument(uri);
const edits = await vscode.commands.executeCommand('vscode.executeFormatDocumentProvider', uri, { tabSize: 2, insertSpaces: true });
if (edits && edits.length > 0) {
const edit = new vscode.WorkspaceEdit();
for (const e of edits) edit.replace(uri, e.range, e.newText);
await vscode.workspace.applyEdit(edit);
webviewView.webview.postMessage({ type: 'refactor-result', action: 'format', success: true, edits: edits.length, id: msg.id });
} else {
webviewView.webview.postMessage({ type: 'refactor-result', action: 'format', success: true, edits: 0, id: msg.id });
}
} catch (e) {
webviewView.webview.postMessage({ type: 'refactor-result', action: 'format', success: false, error: e.message, id: msg.id });
}
// ────── SYSTEM & PROCESS TOOLS ──────
} else if (msg.type === 'system-info') {
webviewView.webview.postMessage({ type: 'system-info-result', info: getSystemInfo(), id: msg.id });
} else if (msg.type === 'list-ports') {
webviewView.webview.postMessage({ type: 'ports-result', ports: getRunningPorts(), id: msg.id });
} else if (msg.type === 'pm2-list') {
try {
const raw = getPm2Services();
const services = JSON.parse(raw || '[]').map(s => ({
name: s.name, id: s.pm_id, status: s.pm2_env?.status, cpu: s.monit?.cpu, mem: ((s.monit?.memory || 0) / 1048576).toFixed(1) + 'MB',
restarts: s.pm2_env?.restart_time, uptime: s.pm2_env?.pm_uptime ? new Date(s.pm2_env.pm_uptime).toISOString() : ''
}));
webviewView.webview.postMessage({ type: 'pm2-result', services, id: msg.id });
} catch (e) {
webviewView.webview.postMessage({ type: 'pm2-result', services: [], error: e.message, id: msg.id });
}
} else if (msg.type === 'disk-usage') {
webviewView.webview.postMessage({ type: 'disk-result', usage: getDiskUsage(), id: msg.id });
} else if (msg.type === 'env-vars') {
webviewView.webview.postMessage({ type: 'env-result', vars: getEnvironmentVars(msg.filter), id: msg.id });
// ────── DATA FORMAT UTILITIES ──────
} else if (msg.type === 'format-json') {
webviewView.webview.postMessage({ type: 'format-result', result: formatJson(msg.text || ''), id: msg.id });
} else if (msg.type === 'compute-hash') {
webviewView.webview.postMessage({ type: 'hash-result', hash: computeHash(msg.text || '', msg.algorithm), algorithm: msg.algorithm || 'sha256', id: msg.id });
} else if (msg.type === 'base64') {
const result = msg.action === 'decode' ? base64Decode(msg.text || '') : base64Encode(msg.text || '');
webviewView.webview.postMessage({ type: 'base64-result', result, action: msg.action, id: msg.id });
} else if (msg.type === 'url-encode') {
const result = msg.action === 'decode' ? urlDecode(msg.text || '') : urlEncode(msg.text || '');
webviewView.webview.postMessage({ type: 'url-encode-result', result, action: msg.action, id: msg.id });
} else if (msg.type === 'generate-uuid') {
webviewView.webview.postMessage({ type: 'uuid-result', uuid: generateUuid(), id: msg.id });
} else if (msg.type === 'count-lines') {
webviewView.webview.postMessage({ type: 'count-result', stats: countLines(msg.filePath || ''), id: msg.id });
// ────── ADVANCED FILE OPS ──────
} else if (msg.type === 'diff-files') {
webviewView.webview.postMessage({ type: 'diff-result', diff: diffFiles(msg.fileA, msg.fileB), id: msg.id });
} else if (msg.type === 'find-replace-all') {
const result = findReplace(msg.filePath, msg.search, msg.replace, msg.isRegex);
webviewView.webview.postMessage({ type: 'find-replace-result', result, id: msg.id });
} else if (msg.type === 'recent-files') {
webviewView.webview.postMessage({ type: 'recent-files-result', files: getRecentFiles(msg.count), id: msg.id });
} else if (msg.type === 'file-sizes') {
webviewView.webview.postMessage({ type: 'file-sizes-result', sizes: getFileSizes(msg.pattern), id: msg.id });
// ────── TEST RUNNER ──────
} else if (msg.type === 'detect-tests') {
webviewView.webview.postMessage({ type: 'test-frameworks', frameworks: detectTestFramework(), id: msg.id });
} else if (msg.type === 'run-tests') {
const result = runTests(msg.framework, msg.testFile);
webviewView.webview.postMessage({ type: 'test-result', result, id: msg.id });
// ────── ANALYSIS TOOLS ──────
} else if (msg.type === 'import-graph') {
webviewView.webview.postMessage({ type: 'import-graph-result', graph: getImportGraph(msg.filePath), id: msg.id });
} else if (msg.type === 'analyze-complexity') {
webviewView.webview.postMessage({ type: 'complexity-result', analysis: analyzeComplexity(msg.filePath), id: msg.id });
} else if (msg.type === 'dependency-info') {
webviewView.webview.postMessage({ type: 'dependency-result', info: getDependencyInfo(), id: msg.id });
} else if (msg.type === 'project-stats') {
webviewView.webview.postMessage({ type: 'project-stats-result', stats: getProjectStats(), id: msg.id });
} else if (msg.type === 'get-diagnostics-detail') {
const diags = vscode.languages.getDiagnostics();
const detailed = [];
for (const [uri, items] of diags) {
for (const d of items) {
detailed.push({
file: vscode.workspace.asRelativePath(uri, false),
line: d.range.start.line + 1, col: d.range.start.character,
severity: d.severity === 0 ? 'error' : d.severity === 1 ? 'warning' : d.severity === 2 ? 'info' : 'hint',
message: d.message, source: d.source || '', code: d.code ? (typeof d.code === 'object' ? d.code.value : d.code) : ''
});
}
}
webviewView.webview.postMessage({ type: 'diagnostics-detail-result', diagnostics: detailed.slice(0, 100), id: msg.id });
// ────── PLATFORM TOOLS ──────
} else if (msg.type === 'update-check') {
checkForUpdates(msg.platform).then(result => {
webviewView.webview.postMessage({ type: 'update-check-result', result, id: msg.id });
});
} else if (msg.type === 'pair-desktop') {
requestDesktopPairing().then(result => {
if (result.error) {
webviewView.webview.postMessage({ type: 'pair-result', result, id: msg.id });
return;
}
webviewView.webview.postMessage({ type: 'pair-code', code: result.pairing_code, pairId: result.pair_id, id: msg.id });
let attempts = 0;
const maxAttempts = 60;
const pollInterval = setInterval(async () => {
attempts++;
if (attempts > maxAttempts) {
clearInterval(pollInterval);
webviewView.webview.postMessage({ type: 'pair-result', result: { error: 'Pairing expired — code was not confirmed within 10 minutes' }, id: msg.id });
return;
}
const status = await pollPairingStatus(result.pair_id);
if (status.status === 'approved' && status.token) {
clearInterval(pollInterval);
savePairedToken(status.token);
webviewView.webview.postMessage({ type: 'pair-result', result: { success: true, message: 'Desktop paired successfully! You are now connected to your gositeme.com account.' }, id: msg.id });
} else if (status.status === 'expired' || status.error) {
clearInterval(pollInterval);
webviewView.webview.postMessage({ type: 'pair-result', result: { error: status.error || 'Pairing code expired' }, id: msg.id });
}
}, 10000);
});
}
});
if (userProfile) {
setTimeout(() => {
webviewView.webview.postMessage({ type: 'user-profile', profile: userProfile });
}, 500);
}
// Push live editor context updates to webview
vscode.window.onDidChangeActiveTextEditor(() => {
if (this.view) {
const fileInfo = getActiveFileDetails();
if (fileInfo) {
this.view.webview.postMessage({ type: 'active-file-changed', file: fileInfo });
}
}
});
vscode.workspace.onDidSaveTextDocument((doc) => {
if (this.view) {
const rel = vscode.workspace.asRelativePath(doc.uri, false);
this.view.webview.postMessage({ type: 'file-saved', file: rel, language: doc.languageId });
}
});
}
}
function transcribeAudio(audioB64, mime) {
if (!audioB64) return Promise.resolve('');
return new Promise((resolve) => {
const raw = Buffer.from(audioB64, 'base64');
if (raw.length < 100) { resolve(''); return; }
const ext = (mime || '').includes('wav') ? 'wav'
: (mime || '').includes('ogg') ? 'ogg'
: (mime || '').includes('mp4') || (mime || '').includes('m4a') ? 'm4a'
: 'webm';
const boundary = '----AlfredSTT' + Date.now();
const filename = 'audio.' + ext;
const contentType = mime || 'audio/webm';
const header = Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="audio"; filename="${filename}"\r\nContent-Type: ${contentType}\r\n\r\n`
);
const modelField = Buffer.from(
`\r\n--${boundary}\r\nContent-Disposition: form-data; name="model"\r\n\r\nwhisper-1\r\n--${boundary}--\r\n`
);
const body = Buffer.concat([header, raw, modelField]);
const req = https.request({
hostname: 'gositeme.com',
port: 443,
path: '/api/stt.php',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': body.length,
'X-Alfred-Source': 'ide-extension'
},
timeout: 60000
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const j = JSON.parse(data);
resolve(j.text || '');
} catch { resolve(''); }
});
});
req.on('error', () => resolve(''));
req.on('timeout', () => { req.destroy(); resolve(''); });
req.write(body);
req.end();
});
}
function generateTTS(text) {
if (!text || text.length < 2) return Promise.resolve(null);
const clean = text.replace(/```[\s\S]*?```/g, ' code block ')
.replace(/[*_`#~\[\]]/g, '')
.replace(/https?:\/\/\S+/g, '')
.replace(/\s+/g, ' ')
.trim()
.substring(0, 4000);
if (clean.length < 2) return Promise.resolve(null);
return new Promise((resolve) => {
const body = JSON.stringify({ text: clean, voice: 'onyx' });
const req = https.request({
hostname: 'gositeme.com',
port: 443,
path: '/api/tts.php',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
'X-Alfred-Source': 'ide-extension'
},
timeout: 30000
}, (res) => {
if (res.statusCode !== 200) { resolve(null); return; }
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
const buf = Buffer.concat(chunks);
if (buf.length < 200) { resolve(null); return; }
const contentType = res.headers['content-type'] || 'audio/mpeg';
resolve('data:' + contentType + ';base64,' + buf.toString('base64'));
});
});
req.on('error', () => resolve(null));
req.on('timeout', () => { req.destroy(); resolve(null); });
req.write(body);
req.end();
});
}
function getEditorContext() {
return buildFullContext();
}
async function queryAlfredAPI(prompt, agent, editorContext, selectedModel = 'sonnet', images = [], pdfFiles = [], attachmentTexts = [], zipFiles = [], multiplier = 30) {
const systemPrompt = buildSystemPrompt(userProfile);
const payload = {
message: prompt, agent: agent || 'alfred',
context: editorContext || '', channel: 'ide-chat',
conv_id: convId || '', model: selectedModel,
token_multiplier: multiplier || 30,
system_prompt: systemPrompt
};
const ideToken = readLocalIdeToken();
const ideIdentity = buildIdeIdentityPayload();
// Duplicate token in JSON body — some stacks strip Authorization / X-Alfred-IDE-Token before PHP sees them.
if (ideToken) {
payload.ide_session_token = ideToken;
}
if (ideIdentity) {
payload.ide_client_id = ideIdentity.ide_client_id;
payload.ide_name = ideIdentity.ide_name;
payload.ide_ts = ideIdentity.ide_ts;
payload.ide_sig = ideIdentity.ide_sig;
payload.ide_email = ideIdentity.ide_email;
}
if (Array.isArray(images) && images.length > 0) {
payload.images = images;
}
if (Array.isArray(pdfFiles) && pdfFiles.length > 0) {
payload.pdf_files = pdfFiles;
}
if (Array.isArray(attachmentTexts) && attachmentTexts.length > 0) {
payload.attachment_texts = attachmentTexts;
}
if (Array.isArray(zipFiles) && zipFiles.length > 0) {
payload.zip_files = zipFiles;
}
const body = JSON.stringify(payload);
const doRequest = (csrf, cookie) => {
return new Promise((resolve, reject) => {
const headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'X-Alfred-Source': 'alfred-ide' };
if (csrf) headers['X-CSRF-Token'] = csrf;
if (cookie) headers['Cookie'] = cookie;
if (ideToken) {
headers['Authorization'] = 'Bearer ' + ideToken;
// Apache/CGI often strips Authorization before PHP — duplicate token for alfred_resolve_ide_bearer_token()
headers['X-Alfred-IDE-Token'] = ideToken;
}
const req = https.request({ hostname: 'gositeme.com', port: 443, path: '/api/alfred-chat.php', method: 'POST', headers, timeout: 120000 }, (res) => {
// Node may give Set-Cookie as a string OR an array — iterating a string walks each CHARACTER and breaks PHPSESSID capture.
const setCookie = res.headers['set-cookie'];
if (setCookie) {
const parts = Array.isArray(setCookie) ? setCookie : [setCookie];
for (let i = 0; i < parts.length; i++) {
const m = String(parts[i]).match(/PHPSESSID=([^;]+)/);
if (m) {
sessionCookie = 'PHPSESSID=' + m[1];
break;
}
}
}
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { console.error('[alfred-commander] Non-JSON response HTTP ' + res.statusCode + ':', data.substring(0, 600)); resolve({ response: data.trim() || 'Alfred returned an empty response. Please send your message again.' }); } });
});
req.on('error', (err) => reject(new Error('API failed: ' + err.message)));
req.on('timeout', () => { req.destroy(); reject(new Error('Timed out (120s)')); });
req.write(body);
req.end();
});
};
let result = await doRequest(csrfToken, sessionCookie);
if (result.csrf_token) csrfToken = result.csrf_token;
for (let csrfAttempt = 0; csrfAttempt < 4 && result && (result.csrf_refresh || result.response === 'Session initialized. Please retry.' || result.error === 'CSRF validation failed'); csrfAttempt++) {
result = await doRequest(csrfToken, sessionCookie);
if (result.csrf_token) csrfToken = result.csrf_token;
}
if (!result || (!result.response && !result.message && !result.error)) {
logTelemetry('empty-response: retrying once');
result = await doRequest(csrfToken, sessionCookie);
if (result && result.csrf_token) csrfToken = result.csrf_token;
}
if (result.conv_id) convId = result.conv_id;
const errText = result && result.error ? String(result.error) : '';
const textOut = (result && (result.response || result.message))
? (result.response || result.message)
: (errText ? ('⚠️ ' + errText) : 'No response from AI');
return {
text: textOut,
agent: (result && result.agent) ? result.agent : agent,
identity: result && result.identity ? result.identity : null,
attachmentReport: (result && result.attachment_report) ? result.attachment_report : []
};
}
let statsPanel = null;
function showStatsPanel(context) {
if (statsPanel) {
statsPanel.reveal(vscode.ViewColumn.One);
return;
}
statsPanel = vscode.window.createWebviewPanel(
'alfredStats',
'Account & Usage Stats',
vscode.ViewColumn.One,
{ enableScripts: true, retainContextWhenHidden: false }
);
statsPanel.onDidDispose(() => { statsPanel = null; });
const ideToken = readLocalIdeToken();
const headers = { 'X-Alfred-Source': 'ide-extension' };
if (ideToken) {
headers['Authorization'] = 'Bearer ' + ideToken;
headers['X-Alfred-IDE-Token'] = ideToken;
}
const req = https.request({
hostname: 'gositeme.com', port: 443,
path: '/api/alfred-ide-session.php?action=stats',
method: 'GET', headers, timeout: 15000
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const stats = JSON.parse(data);
if (statsPanel) {
statsPanel.webview.html = getStatsHtml(stats);
}
} catch (e) {
if (statsPanel) {
statsPanel.webview.html = getStatsHtml({ valid: false, error: 'Failed to load stats: ' + e.message });
}
}
});
});
req.on('error', (e) => {
if (statsPanel) {
statsPanel.webview.html = getStatsHtml({ valid: false, error: 'Connection error: ' + e.message });
}
});
req.on('timeout', () => { req.destroy(); });
req.end();
// Show loading state immediately
statsPanel.webview.html = getStatsHtml(null);
}
function getStatsHtml(stats) {
if (!stats) {
return `<!DOCTYPE html><html><head><meta charset="UTF-8">
<style>body{font-family:'Segoe UI',system-ui,sans-serif;background:#0d1117;color:#c9d1d9;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;}
.loader{text-align:center;}.spin{display:inline-block;width:40px;height:40px;border:3px solid #30363d;border-top-color:#e2b340;border-radius:50%;animation:spin 0.8s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}</style></head>
<body><div class="loader"><div class="spin"></div><p style="margin-top:16px;color:#8b949e;">Loading your stats...</p></div></body></html>`;
}
if (!stats.valid) {
return `<!DOCTYPE html><html><head><meta charset="UTF-8">
<style>body{font-family:'Segoe UI',system-ui,sans-serif;background:#0d1117;color:#c9d1d9;padding:40px;margin:0;}</style></head>
<body><h2 style="color:#e2b340;">Account Stats</h2><p style="color:#f85149;margin-top:16px;">` + escHtml(stats.error || 'Unable to load stats') + `</p></body></html>`;
}
const name = escHtml(stats.name || 'User');
const email = escHtml(stats.email || '');
const plan = (stats.plan || 'free').toLowerCase();
const planDisplay = plan === 'commander' ? 'Commander' : (stats.plan || 'Free').charAt(0).toUpperCase() + (stats.plan || 'free').slice(1);
const planColors = { commander: '#e2b340', free: '#6a737d', starter: '#3b82f6', professional: '#a855f7', enterprise: '#22c55e' };
const planColor = planColors[plan] || '#6a737d';
const tokensUsed = stats.tokens_used || 0;
const tokensIncluded = stats.tokens_included || 50000;
const tokensOverage = stats.tokens_overage || 0;
const costOverage = stats.cost_overage_usd || 0;
const isUnlimited = stats.unlimited || plan === 'commander';
const remaining = Math.max(0, tokensIncluded - tokensUsed);
const pct = tokensIncluded > 0 ? Math.min(100, Math.round((tokensUsed / tokensIncluded) * 100)) : 0;
const barColor = pct >= 90 ? '#ef4444' : pct >= 70 ? '#f59e0b' : '#3b82f6';
const fmtK = (n) => n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'K' : String(n);
// Services section
let servicesHtml = '';
if (stats.services && stats.services.length > 0) {
servicesHtml = stats.services.map(s =>
`<tr><td>${escHtml(s.product || 'Service')}</td><td>${escHtml(s.domain || '—')}</td><td class="status-active">${escHtml(s.status)}</td><td>$${parseFloat(s.amount || 0).toFixed(2)}/${escHtml(s.billing_cycle || 'N/A')}</td></tr>`
).join('');
} else {
servicesHtml = '<tr><td colspan="4" style="color:#8b949e;text-align:center;">No active services</td></tr>';
}
// Recent invoices
let invoicesHtml = '';
if (stats.recent_invoices && stats.recent_invoices.length > 0) {
invoicesHtml = stats.recent_invoices.map(i => {
const statusClass = i.status === 'Paid' ? 'status-active' : i.status === 'Unpaid' ? 'status-unpaid' : 'status-other';
return `<tr><td>#${escHtml(i.invoice_number || String(i.id))}</td><td>$${parseFloat(i.total || 0).toFixed(2)}</td><td class="${statusClass}">${escHtml(i.status)}</td><td>${escHtml((i.due_date || i.created_at || '').substring(0, 10))}</td></tr>`;
}).join('');
} else {
invoicesHtml = '<tr><td colspan="4" style="color:#8b949e;text-align:center;">No invoices</td></tr>';
}
// Usage breakdown
let usageBreakdownHtml = '';
if (stats.usage_breakdown && stats.usage_breakdown.length > 0) {
usageBreakdownHtml = stats.usage_breakdown.map(u =>
`<tr><td>${escHtml(u.feature)}</td><td>${escHtml(u.model)}</td><td>${fmtK(parseInt(u.input_tokens || 0) + parseInt(u.output_tokens || 0))}</td><td>${parseInt(u.requests || 0)}</td><td>$${parseFloat(u.cost_usd || 0).toFixed(4)}</td></tr>`
).join('');
} else {
usageBreakdownHtml = '<tr><td colspan="5" style="color:#8b949e;text-align:center;">No usage this period</td></tr>';
}
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0d1117; color: #c9d1d9; padding: 32px; max-width: 900px; margin: 0 auto; }
h1 { color: #e2b340; font-size: 22px; font-weight: 600; margin-bottom: 4px; }
.subtitle { color: #8b949e; font-size: 13px; margin-bottom: 24px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 20px; }
.card-label { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
.card-value { font-size: 24px; font-weight: 700; }
.card-sub { font-size: 11px; color: #8b949e; margin-top: 4px; }
.usage-section { background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 20px; margin-bottom: 24px; }
.usage-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.usage-title { font-size: 14px; font-weight: 600; }
.plan-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.usage-bar-outer { height: 8px; border-radius: 99px; background: #21262d; overflow: hidden; margin-bottom: 8px; }
.usage-bar-fill { height: 100%; border-radius: 99px; transition: width 0.5s ease; }
.usage-detail { font-size: 12px; color: #8b949e; }
.section-title { font-size: 15px; font-weight: 600; color: #e2b340; margin-bottom: 12px; margin-top: 8px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 24px; }
th { text-align: left; font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.3px; padding: 8px 12px; border-bottom: 1px solid #30363d; }
td { padding: 10px 12px; border-bottom: 1px solid #21262d; font-size: 13px; }
tr:hover td { background: rgba(226,179,64,0.04); }
.status-active { color: #3fb950; font-weight: 600; }
.status-unpaid { color: #f85149; font-weight: 600; }
.status-other { color: #8b949e; }
.refresh-btn { background: #161b22; border: 1px solid #30363d; color: #c9d1d9; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 12px; float: right; }
.refresh-btn:hover { border-color: #e2b340; color: #e2b340; }
.footer { text-align: center; color: #484f58; font-size: 11px; margin-top: 24px; padding-top: 16px; border-top: 1px solid #21262d; }
</style>
</head>
<body>
<h1>$(account) ${name}</h1>
<div class="subtitle">${email} · Last login: ${escHtml((stats.last_login || '').substring(0, 16))}</div>
<div class="cards">
<div class="card">
<div class="card-label">Plan</div>
<div class="card-value" style="color:${planColor}">${planDisplay}</div>
</div>
<div class="card">
<div class="card-label">Credit Balance</div>
<div class="card-value" style="color:#3fb950">$${escHtml(stats.credit_balance || '0.00')}</div>
</div>
<div class="card">
<div class="card-label">Active Services</div>
<div class="card-value">${stats.active_services || 0}</div>
</div>
<div class="card">
<div class="card-label">Unpaid Invoices</div>
<div class="card-value" style="color:${(stats.unpaid_invoices || 0) > 0 ? '#f85149' : '#3fb950'}">${stats.unpaid_invoices || 0}</div>
<div class="card-sub">${(stats.unpaid_invoices || 0) > 0 ? 'Total due: $' + escHtml(stats.total_due || '0.00') : 'All clear'}</div>
</div>
</div>
<div class="usage-section">
<div class="usage-header">
<span class="usage-title">Token Usage — ${new Date().toLocaleString('en-US', { month: 'long', year: 'numeric' })}</span>
<span class="plan-badge" style="background:${planColor}20;color:${planColor};border:1px solid ${planColor}40">${planDisplay}</span>
</div>
${isUnlimited
? `<div class="usage-bar-outer"><div class="usage-bar-fill" style="width:100%;background:#e2b340;"></div></div>
<div class="usage-detail">Unlimited usage — Commander plan</div>`
: `<div class="usage-bar-outer"><div class="usage-bar-fill" style="width:${pct}%;background:${barColor};"></div></div>
<div class="usage-detail">${fmtK(tokensUsed)} used of ${fmtK(tokensIncluded)} included (${fmtK(remaining)} remaining)${tokensOverage > 0 ? ' · Overage: ' + fmtK(tokensOverage) + ' tokens ($' + costOverage.toFixed(2) + ')' : ''}</div>`
}
</div>
<div class="section-title">Usage Breakdown</div>
<table>
<thead><tr><th>Feature</th><th>Model</th><th>Tokens</th><th>Requests</th><th>Cost</th></tr></thead>
<tbody>${usageBreakdownHtml}</tbody>
</table>
<div class="section-title">Active Services</div>
<table>
<thead><tr><th>Product</th><th>Domain</th><th>Status</th><th>Amount</th></tr></thead>
<tbody>${servicesHtml}</tbody>
</table>
<div class="section-title">Recent Invoices</div>
<table>
<thead><tr><th>Invoice</th><th>Amount</th><th>Status</th><th>Date</th></tr></thead>
<tbody>${invoicesHtml}</tbody>
</table>
<div class="footer">Alfred · GoSiteMe · Data refreshed at ${new Date().toLocaleTimeString()}</div>
</body>
</html>`;
}
function escHtml(str) {
return String(str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function getWebviewContent(injectedToken, injectedIdentity) {
const safeToken = (injectedToken || '').replace(/[^a-f0-9]/g, '');
const safeIdentity = JSON.stringify(injectedIdentity || {});
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' https: wss: ws: data: blob:; connect-src 'self' https://gositeme.com https: wss: ws:; img-src 'self' data: blob: https:; font-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--vscode-editor-background, #0d1117); color: var(--vscode-editor-foreground, #c9d1d9); padding: 10px; height: 100vh; display: flex; flex-direction: column; }
.header { text-align: center; padding: 6px 0 10px; border-bottom: 1px solid var(--vscode-panel-border, #1a1a2e); margin-bottom: 8px; }
.header h2 { color: #e2b340; font-size: 13px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; }
.header .subtitle { color: var(--vscode-descriptionForeground, #6a737d); font-size: 10px; margin-top: 2px; }
.identity-badge { margin-top: 4px; display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--vscode-input-border, #30363d); color: var(--vscode-descriptionForeground, #8b949e); }
.identity-badge.verified { color: #0d1117; background: #22c55e; border-color: #22c55e; }
.identity-badge.guest { color: #f59e0b; border-color: #f59e0b; }
.usage-info { display: none; font-size: 10px; margin-top: 4px; color: var(--vscode-descriptionForeground, #8b949e); text-align: center; }
.usage-info .plan-label { font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; font-size: 9px; }
.usage-info .plan-commander { color: #e2b340; }
.usage-info .plan-free { color: #6a737d; }
.usage-info .plan-starter { color: #3b82f6; }
.usage-info .plan-professional { color: #a855f7; }
.usage-info .plan-enterprise { color: #22c55e; }
.usage-bar { height: 4px; border-radius: 99px; background: rgba(255,255,255,0.08); overflow: hidden; margin-top: 3px; width: 100%; }
.usage-fill { height: 100%; border-radius: 99px; background: #3b82f6; transition: width 0.3s ease; }
.usage-fill.warn { background: #f59e0b; }
.usage-fill.danger { background: #ef4444; }
.usage-fill.unlimited { background: #e2b340; width: 100% !important; }
/* One compact row; wraps on narrow sidebars without extra vertical chrome */
.agent-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 6px; }
.agent-bar > label { font-size: 9px; color: var(--vscode-descriptionForeground); text-transform: uppercase; letter-spacing: 0.4px; flex-shrink: 0; }
.agent-select, .model-select {
flex: 1 1 42%; min-width: 0; min-height: 26px;
background: var(--vscode-input-background, #161b22); border: 1px solid var(--vscode-input-border, #30363d); border-radius: 6px;
color: var(--vscode-input-foreground, #c9d1d9); padding: 4px 6px; font-size: 11px; outline: none; cursor: pointer;
}
.model-select { color: #3b82f6; }
.multiplier-select { flex: 0 0 92px; color: #e2b340; }
.agent-select:focus { border-color: #e2b340; }
.model-select:focus { border-color: #3b82f6; }
.model-select option, .model-select optgroup { background: #161b22; color: #c9d1d9; }
.chat-area { flex: 1; overflow-y: auto; margin-bottom: 8px; padding-right: 4px; }
.chat-area::-webkit-scrollbar { width: 4px; }
.chat-area::-webkit-scrollbar-thumb { background: #1a1a2e; border-radius: 2px; }
.message { margin-bottom: 8px; padding: 7px 9px; border-radius: 8px; font-size: 12px; line-height: 1.5; animation: fadeIn 0.3s ease; word-wrap: break-word; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.message.commander { background: rgba(226,179,64,0.08); border-left: 3px solid #e2b340; }
.message.alfred { background: rgba(59,130,246,0.08); border-left: 3px solid #3b82f6; }
.message.system { background: rgba(35,134,54,0.08); border-left: 3px solid #238636; color: #7ee787; font-size: 11px; padding: 5px 8px; }
.message .sender { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
.message.commander .sender { color: #e2b340; }
.message.alfred .sender { color: #3b82f6; }
.message pre { background: var(--vscode-textCodeBlock-background, #0d1117); border: 1px solid var(--vscode-panel-border, #30363d); border-radius: 4px; padding: 6px 8px; margin: 4px 0; overflow-x: auto; font-size: 11px; font-family: var(--vscode-editor-font-family, monospace); }
.message code { background: rgba(255,255,255,0.05); padding: 1px 4px; border-radius: 3px; font-size: 11px; }
/* Input full width, then a short action row — avoids cramming mic + attach + send beside the field */
.controls-wrap { display: flex; flex-direction: column; gap: 6px; width: 100%; min-width: 0; }
.controls-wrap .text-input { width: 100%; flex: none; min-width: 0; }
.action-row { display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0; flex-wrap: nowrap; }
.action-row .mic-btn { flex-shrink: 0; }
.action-row .attach-btn { flex-shrink: 0; }
.action-row .send-btn { margin-left: auto; flex-shrink: 0; }
.mic-btn { width: 32px; height: 32px; border-radius: 50%; border: 2px solid #e2b340; background: transparent; color: #e2b340; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s; flex-shrink: 0; }
.mic-btn:hover { background: #e2b340; color: #0d1117; }
.mic-btn.recording { background: #e53e3e; border-color: #e53e3e; color: #fff; animation: pulse 1.5s infinite; }
.mic-btn.transcribing { background: #3b82f6; border-color: #3b82f6; color: #fff; animation: pulse 1s infinite; }
@keyframes pulse { 0%, 100% { box-shadow: 0 0 8px rgba(226,179,64,0.3); } 50% { box-shadow: 0 0 20px rgba(226,179,64,0.6); } }
.mic-btn svg { width: 16px; height: 16px; }
.text-input { background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, #30363d); border-radius: 8px; color: var(--vscode-input-foreground); padding: 7px 10px; font-size: 12px; outline: none; }
.text-input:focus { border-color: #e2b340; }
.text-input::placeholder { color: var(--vscode-input-placeholderForeground, #484f58); }
.send-btn { background: #e2b340; border: none; color: #0d1117; border-radius: 8px; padding: 6px 14px; cursor: pointer; font-weight: 700; font-size: 11px; }
.send-btn:hover { opacity: 0.85; }
.bottom-bar { display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap; margin-top: 4px; gap: 6px; row-gap: 4px; position: relative; z-index: 5; pointer-events: auto; }
.attach-btn { width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--vscode-input-border, #30363d); background: transparent; color: var(--vscode-descriptionForeground, #6a737d); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 14px; }
.attach-btn:hover { border-color: #e2b340; color: #e2b340; }
.image-preview-strip { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 4px; }
.image-preview-strip.empty { display: none; }
.attach-panel { border: 1px solid var(--vscode-input-border, #30363d); border-radius: 6px; padding: 6px; margin-bottom: 6px; background: rgba(255,255,255,0.02); }
.attach-panel.empty { display: none; }
.attach-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; font-size: 11px; }
.attach-row:last-child { margin-bottom: 0; }
.attach-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.attach-meta { color: var(--vscode-descriptionForeground, #6a737d); font-size: 10px; }
.attach-state { border: 1px solid var(--vscode-input-border, #30363d); border-radius: 999px; padding: 1px 6px; font-size: 9px; color: var(--vscode-descriptionForeground, #8b949e); white-space: nowrap; }
.attach-state.ready { color: #60a5fa; border-color: #60a5fa; }
.attach-state.queued { color: #f59e0b; border-color: #f59e0b; }
.attach-state.ok { color: #22c55e; border-color: #22c55e; }
.attach-state.warn { color: #f59e0b; border-color: #f59e0b; }
.attach-state.error { color: #ef4444; border-color: #ef4444; }
.attach-detail { color: var(--vscode-descriptionForeground, #6a737d); font-size: 9px; margin-left: 2px; max-width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.small-btn { border: 1px solid var(--vscode-input-border, #30363d); background: transparent; color: var(--vscode-foreground, #c9d1d9); border-radius: 4px; cursor: pointer; font-size: 10px; padding: 1px 5px; }
.small-btn:hover { border-color: #e2b340; color: #e2b340; }
.retry-btn { margin-top: 6px; }
.meter { font-size: 9px; color: var(--vscode-descriptionForeground, #6a737d); margin-top: 2px; }
.meter .bar { height: 4px; border-radius: 999px; background: rgba(255,255,255,0.08); overflow: hidden; margin-top: 2px; }
.meter .fill { height: 100%; width: 0%; background: #3b82f6; transition: width .2s ease; }
.telemetry-drawer { margin-top: 6px; border: 1px solid var(--vscode-input-border, #30363d); border-radius: 6px; max-height: 120px; overflow: auto; padding: 6px; font-size: 10px; background: rgba(0,0,0,0.15); }
.telemetry-drawer.hidden { display: none; }
.telemetry-line { margin-bottom: 3px; color: var(--vscode-descriptionForeground, #8b949e); }
.img-thumb { position: relative; width: 48px; height: 48px; border-radius: 4px; overflow: hidden; border: 1px solid var(--vscode-input-border, #30363d); flex-shrink: 0; }
.img-thumb img { width: 100%; height: 100%; object-fit: cover; }
.img-thumb .remove-img { position: absolute; top: 0; right: 0; background: rgba(0,0,0,0.7); color: #fff; border: none; cursor: pointer; font-size: 10px; padding: 1px 3px; line-height: 1; }
.status { font-size: 10px; color: var(--vscode-descriptionForeground, #484f58); flex: 1; }
.status.active { color: #e2b340; }
.ctrl-btn { background: none; border: 1px solid var(--vscode-input-border, #30363d); border-radius: 4px; font-size: 9px; padding: 2px 6px; cursor: pointer; font-family: inherit; white-space: nowrap; }
.ctrl-btn.voice-btn { color: #e2b340; }
.ctrl-btn.voice-btn.off { color: #484f58; }
.ctrl-btn.hf-btn { color: #484f58; }
.ctrl-btn.hf-btn.active { color: #0d1117; background: #22c55e; border-color: #22c55e; animation: hfPulse 2s infinite; }
@keyframes hfPulse { 0%, 100% { box-shadow: 0 0 4px rgba(34,197,94,0.3); } 50% { box-shadow: 0 0 12px rgba(34,197,94,0.6); } }
.thinking { display: inline-block; color: #3b82f6; }
.thinking::after { content: ''; animation: dots 1.5s infinite; }
@keyframes dots { 0% { content: '.'; } 33% { content: '..'; } 66% { content: '...'; } }
.ide-quick-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; padding: 8px 10px; border: 1px solid var(--vscode-input-border, #30363d); border-radius: 8px; background: rgba(226,179,64,0.07); }
.ide-quick-label { font-size: 9px; color: var(--vscode-descriptionForeground); text-transform: uppercase; letter-spacing: 0.5px; margin-right: 4px; flex-shrink: 0; }
.ide-q-btn { font-size: 10px; padding: 5px 10px; border-radius: 6px; border: 1px solid var(--vscode-input-border, #30363d); background: var(--vscode-input-background, #161b22); color: var(--vscode-foreground); cursor: pointer; white-space: nowrap; }
.ide-q-btn:hover { border-color: #e2b340; color: #e2b340; }
.code-block-wrap { margin: 6px 0; border: 1px solid var(--vscode-panel-border, #30363d); border-radius: 6px; overflow: hidden; }
.code-block-header { display: flex; align-items: center; gap: 4px; padding: 4px 8px; background: rgba(255,255,255,0.03); border-bottom: 1px solid var(--vscode-panel-border, #30363d); }
.code-lang { font-size: 9px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; flex: 1; }
.cb-btn { font-size: 9px; padding: 2px 8px; border-radius: 4px; border: 1px solid var(--vscode-input-border, #30363d); background: transparent; cursor: pointer; font-family: inherit; }
.cb-copy { color: #60a5fa; }
.cb-copy:hover { background: rgba(96,165,250,0.15); border-color: #60a5fa; }
.cb-insert { color: #22c55e; }
.cb-insert:hover { background: rgba(34,197,94,0.15); border-color: #22c55e; }
.cb-run { color: #e2b340; }
.cb-run:hover { background: rgba(226,179,64,0.15); border-color: #e2b340; }
</style>
</head>
<body>
<div class="header">
<h2>Alfred</h2>
<div class="subtitle">All models · Voice STT/TTS · Attachments · Ctrl+Shift+Alt+A · Enter sends</div>
<div class="identity-badge" id="identityBadge" style="display:none">Identity</div>
<div class="usage-info" id="usageInfo">
<span class="plan-label" id="planLabel">Plan</span> · <span id="usageText">Loading...</span>
<div class="usage-bar"><div class="usage-fill" id="usageFill"></div></div>
</div>
</div>
<div class="agent-bar">
<label for="agentSelect">Agent</label>
<select class="agent-select" id="agentSelect" title="Routing persona — applies on Send">
<option value="alfred" selected>Alfred (General)</option>
<option value="cipher">Cipher (Security)</option>
<option value="sage">Sage (Knowledge)</option>
<option value="nova">Nova (Creative)</option>
<option value="atlas">Atlas (Strategy)</option>
<option value="architect">Architect (Engineering)</option>
<option value="sentinel">Sentinel (Monitoring)</option>
<option value="catalyst">Catalyst (Growth)</option>
<option value="oracle">Oracle (Analytics)</option>
<option value="scout">Scout (Research)</option>
</select>
<label for="modelSelect">Model</label>
<select class="model-select" id="modelSelect" title="LLM — applies on Send">
<optgroup label="💜 Anthropic">
<option value="sonnet" selected>💜 Sonnet 4.6</option>
<option value="haiku">⚡ Haiku 4.5 (Fast)</option>
<option value="opus">👑 Opus 4.6 (Max)</option>
</optgroup>
<optgroup label="🟢 OpenAI">
<option value="gpt-4.1">🟢 GPT-4.1</option>
<option value="gpt-4.1-mini">🟡 GPT-4.1 Mini</option>
<option value="gpt-4.1-nano">🟤 GPT-4.1 Nano</option>
<option value="gpt-4o">🔵 GPT-4o</option>
<option value="gpt-4o-mini">🔹 GPT-4o Mini</option>
</optgroup>
<optgroup label="💎 Google">
<option value="gemini-3.1-pro">🌐 Gemini 3.1 Pro</option>
<option value="gemini-3-flash">⚡ Gemini 3 Flash</option>
<option value="gemini-2.5-pro">💎 Gemini 2.5 Pro</option>
<option value="gemini-2.5-flash">💡 Gemini 2.5 Flash</option>
</optgroup>
<optgroup label="🔧 Open Source">
<option value="turbo">🔧 Qwen3 Coder (Cheap)</option>
<option value="deepseek-v3.1">🌊 DeepSeek V3.1</option>
<option value="deepseek-r1">🧪 DeepSeek R1 (Reasoning)</option>
<option value="llama-4-maverick">🦙 Llama 4 Maverick</option>
<option value="llama-4-scout">🔭 Llama 4 Scout</option>
<option value="qwen3-coder-480b">🏗️ Qwen3 Coder 480B</option>
<option value="mistral-small">🇫🇷 Mistral Small</option>
</optgroup>
<optgroup label="🆓 Groq (Free)">
<option value="groq-llama-3.3">🆓 Llama 3.3 70B</option>
<option value="groq-llama-3.1">🆓 Llama 3.1 8B</option>
</optgroup>
<optgroup label="🤖 Auto">
<option value="auto">🤖 Auto (Smart Route)</option>
</optgroup>
</select>
<label for="multiplierSelect">Tokens</label>
<select class="multiplier-select" id="multiplierSelect" title="Token multiplier — scales max response length">
<option value="1">1x</option>
<option value="30" selected>30x</option>
<option value="60">60x</option>
<option value="120">120x</option>
<option value="300">300x</option>
<option value="600">600x</option>
</select>
</div>
<div class="ide-quick-bar">
<span class="ide-quick-label">IDE</span>
<button type="button" class="ide-q-btn" id="acIdeTerminal" title="New terminal">Terminal</button>
<button type="button" class="ide-q-btn" id="acIdeSave" title="Save file">Save</button>
<button type="button" class="ide-q-btn" id="acIdeSaveAll" title="Save all">Save all</button>
<button type="button" class="ide-q-btn" id="acIdePalette" title="Command palette">Commands</button>
<button type="button" class="ide-q-btn" id="acIdeSplit" title="Split editor">Split</button>
<button type="button" class="ide-q-btn" id="acIdeNew" title="New untitled file">New file</button>
<button type="button" class="ide-q-btn" id="acIdeGit" title="Open source control">Git</button>
<button type="button" class="ide-q-btn" id="acIdeProblems" title="Show problems panel">Problems</button>
<button type="button" class="ide-q-btn" id="acIdeSearch" title="Search across files">Search</button>
<button type="button" class="ide-q-btn" id="acIdeFormat" title="Format document">Format</button>
</div>
<div class="chat-area" id="chatArea">
<div class="message alfred" id="greetingMsg">
<div class="sender">Alfred</div>
<div>Initializing workspace intelligence...</div>
</div>
</div>
<div class="attach-panel empty" id="attachPanel"></div>
<div class="image-preview-strip empty" id="imagePreviewStrip"></div>
<div class="controls-wrap">
<input type="text" class="text-input" id="textInput" placeholder="Ask Alfred… (Ctrl+V to paste image)" autocomplete="off">
<div class="action-row">
<button type="button" class="mic-btn" id="micBtn" title="Voice input (Whisper STT)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</button>
<button type="button" class="attach-btn" id="attachBtn" title="Attach image, PDF, code, text, or ZIP">📎</button>
<input type="file" id="fileInput" accept="image/*,.pdf,.zip,text/*,.md,.txt,.json,.js,.jsx,.ts,.tsx,.php,.py,.html,.css,.scss,.xml,.yml,.yaml,.sh,.log,.csv,.sql" multiple style="display:none">
<button type="button" class="send-btn" id="ac-send" title="Send to Alfred API — not a link" onclick="if(typeof dispatchSend==='function'){dispatchSend(event)}else{document.getElementById('status').textContent='Script not loaded — refresh page'}">SEND</button>
</div>
</div>
<div class="meter" id="payloadMeter">Payload: 0 KB
<div class="bar"><div class="fill" id="payloadFill"></div></div>
</div>
<div class="bottom-bar">
<div class="status" id="status">Ready</div>
<button class="ctrl-btn voice-btn" id="voiceToggle">Voice ON</button>
<button class="ctrl-btn hf-btn" id="handsFreeBtn" title="Hands-free: auto-listen after Alfred speaks">Hands-Free</button>
</div>
<script>
let vscode;
try { vscode = acquireVsCodeApi(); } catch(e) { vscode = { postMessage: function(){}, setState: function(){}, getState: function(){} }; }
// Injected session data from extension host (server-side read of session.json)
window.__alfredToken = '${safeToken}';
window.__alfredIdentity = ${safeIdentity};
window.__alfredCsrf = null;
window.__alfredSessionCookie = null;
const chatArea = document.getElementById('chatArea');
const micBtn = document.getElementById('micBtn');
const textInput = document.getElementById('textInput');
const sendBtn = document.getElementById('ac-send');
const statusEl = document.getElementById('status');
const agentSelect = document.getElementById('agentSelect');
const modelSelect = document.getElementById('modelSelect');
const multiplierSelect = document.getElementById('multiplierSelect');
const handsFreeBtn = document.getElementById('handsFreeBtn');
const attachBtn = document.getElementById('attachBtn');
const fileInput = document.getElementById('fileInput');
const imagePreviewStrip = document.getElementById('imagePreviewStrip');
const attachPanel = document.getElementById('attachPanel');
const payloadMeter = document.getElementById('payloadMeter');
const payloadFill = document.getElementById('payloadFill');
const identityBadge = document.getElementById('identityBadge');
let isRecording = false, isTranscribing = false, isSpeaking = false, messageId = 0;
let voiceEnabled = true, currentAudio = null;
let handsFreeMode = false;
let mediaRecorder = null, audioChunks = [], mediaStream = null;
let silenceTimer = null;
let sttIdCounter = 0;
let pendingSttResolve = null;
let pendingImages = []; // { id, name, base64, mime, dataUrl, size }
let pendingPdfFiles = []; // { id, name, data, size }
let pendingTextFiles = []; // { id, name, text, size }
let pendingZipFiles = []; // { id, name, data, size }
let pendingAttachments = []; // ordered queue of ids
let attachmentIdCounter = 0;
let pendingAttachmentReads = 0;
let queuedSend = false;
let lastRequest = null;
function createAttachmentReadGuard(label, timeoutMs) {
pendingAttachmentReads += 1;
let done = false;
const timer = setTimeout(() => {
if (done) return;
done = true;
if (pendingAttachmentReads > 0) pendingAttachmentReads -= 1;
setStatus(label + ' processing timed out');
logTelemetry('attachment-timeout: ' + label);
if (pendingAttachmentReads === 0 && queuedSend) {
queuedSend = false;
dispatchSend();
}
}, timeoutMs || 15000);
return () => {
if (done) return;
done = true;
clearTimeout(timer);
finalizeAttachmentRead();
};
}
function logTelemetry(line) {
const telemetryDrawer = null;
if (!telemetryDrawer) return;
const ts = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.className = 'telemetry-line';
div.textContent = '[' + ts + '] ' + line;
telemetryDrawer.appendChild(div);
while (telemetryDrawer.childElementCount > 120) {
telemetryDrawer.removeChild(telemetryDrawer.firstChild);
}
telemetryDrawer.scrollTop = telemetryDrawer.scrollHeight;
}
function bytesToKB(n) {
return Math.round((n || 0) / 1024);
}
function updatePayloadMeter() {
if (!payloadMeter || !payloadFill) return;
let total = 0;
for (const i of pendingImages) total += i.size || 0;
for (const p of pendingPdfFiles) total += p.size || 0;
for (const t of pendingTextFiles) total += t.size || 0;
for (const z of pendingZipFiles) total += z.size || 0;
const kb = bytesToKB(total);
if (payloadMeter.firstChild) payloadMeter.firstChild.textContent = 'Payload: ' + kb + ' KB';
const pct = Math.min(100, Math.round((total / (10 * 1024 * 1024)) * 100));
payloadFill.style.width = pct + '%';
}
function applyIdentity(profile) {
const p = profile || {};
const name = p.name || p.user || 'User';
if (!identityBadge) return;
identityBadge.textContent = 'Identity: ' + name;
const verified = p.verified === true || !!p.client_id;
const guestish = !verified || String(name).toLowerCase() === 'guest';
identityBadge.classList.toggle('guest', guestish);
identityBadge.classList.toggle('verified', !guestish);
identityBadge.style.display = !guestish ? 'inline-block' : 'none';
logTelemetry('identity: ' + name + (p.client_id ? ' (id=' + p.client_id + ')' : '') + (verified ? ' [verified]' : ' [guest]'));
// Usage/balance display
const usageInfo = document.getElementById('usageInfo');
const planLabel = document.getElementById('planLabel');
const usageText = document.getElementById('usageText');
const usageFill = document.getElementById('usageFill');
if (!usageInfo || !planLabel || !usageText || !usageFill) return;
if (guestish || !p.plan) { usageInfo.style.display = 'none'; return; }
usageInfo.style.display = 'block';
const plan = (p.plan || 'free').toLowerCase();
planLabel.textContent = plan === 'commander' ? 'Commander' : (p.plan || 'Free');
planLabel.className = 'plan-label plan-' + plan;
if (p.unlimited || plan === 'commander') {
usageText.textContent = 'Unlimited';
usageFill.className = 'usage-fill unlimited';
usageFill.style.width = '100%';
} else {
const used = p.tokens_used || 0;
const included = p.tokens_included || 50000;
const remaining = Math.max(0, included - used);
const pct = included > 0 ? Math.min(100, Math.round((used / included) * 100)) : 0;
const fmtK = (n) => n >= 1000000 ? (n / 1000000).toFixed(1) + 'M' : n >= 1000 ? (n / 1000).toFixed(0) + 'K' : String(n);
usageText.textContent = fmtK(remaining) + ' tokens left (' + fmtK(used) + ' / ' + fmtK(included) + ')';
usageFill.style.width = pct + '%';
usageFill.className = 'usage-fill' + (pct >= 90 ? ' danger' : pct >= 70 ? ' warn' : '');
}
}
function findAttachmentById(id) {
const img = pendingImages.find(x => x.id === id);
if (img) return { kind: 'image', item: img };
const pdf = pendingPdfFiles.find(x => x.id === id);
if (pdf) return { kind: 'pdf', item: pdf };
const txt = pendingTextFiles.find(x => x.id === id);
if (txt) return { kind: 'text', item: txt };
const zip = pendingZipFiles.find(x => x.id === id);
if (zip) return { kind: 'zip', item: zip };
return null;
}
function setAttachmentStatusById(id, status, detail) {
const ref = findAttachmentById(id);
if (!ref || !ref.item) return;
ref.item.status = status || 'ready';
ref.item.detail = detail || '';
}
function summarizeAttachmentReport(report) {
if (!Array.isArray(report) || report.length === 0) return '';
return report.map((item) => {
const name = item && item.name ? item.name : (item && item.type ? item.type : 'attachment');
const status = item && item.status ? item.status : 'processed';
const detail = item && item.detail ? item.detail : '';
return name + ': ' + status + (detail ? ' (' + detail + ')' : '');
}).join(' | ');
}
function applyAttachmentReport(report) {
if (!Array.isArray(report) || report.length === 0) return;
for (const item of report) {
const name = String(item && item.name ? item.name : '');
const ref = pendingAttachments.map((id) => findAttachmentById(id)).find((entry) => entry && entry.item && entry.item.name === name);
if (!ref) continue;
ref.item.status = item.status || 'ok';
ref.item.detail = item.detail || '';
}
renderAttachmentPanel();
const summary = summarizeAttachmentReport(report);
if (summary) {
addMessage('system', 'Attachment report: ' + summary);
}
}
function removeAttachmentById(id) {
pendingImages = pendingImages.filter(x => x.id !== id);
pendingPdfFiles = pendingPdfFiles.filter(x => x.id !== id);
pendingTextFiles = pendingTextFiles.filter(x => x.id !== id);
pendingZipFiles = pendingZipFiles.filter(x => x.id !== id);
pendingAttachments = pendingAttachments.filter(x => x !== id);
renderAttachmentPanel();
updatePayloadMeter();
}
function moveAttachment(id, dir) {
const idx = pendingAttachments.indexOf(id);
if (idx < 0) return;
const ni = idx + dir;
if (ni < 0 || ni >= pendingAttachments.length) return;
const tmp = pendingAttachments[idx];
pendingAttachments[idx] = pendingAttachments[ni];
pendingAttachments[ni] = tmp;
renderAttachmentPanel();
}
function renderAttachmentPanel() {
if (!attachPanel || !imagePreviewStrip) return;
attachPanel.innerHTML = '';
if (pendingAttachments.length === 0) {
attachPanel.classList.add('empty');
imagePreviewStrip.classList.add('empty');
imagePreviewStrip.innerHTML = '';
return;
}
attachPanel.classList.remove('empty');
imagePreviewStrip.innerHTML = '';
const previewImgs = pendingAttachments
.map(id => findAttachmentById(id))
.filter(x => x && x.kind === 'image')
.slice(0, 6);
if (previewImgs.length > 0) {
imagePreviewStrip.classList.remove('empty');
for (const ref of previewImgs) {
const thumb = document.createElement('div');
thumb.className = 'img-thumb';
const img = document.createElement('img');
img.src = ref.item.dataUrl;
thumb.appendChild(img);
imagePreviewStrip.appendChild(thumb);
}
} else {
imagePreviewStrip.classList.add('empty');
}
for (let i = 0; i < pendingAttachments.length; i++) {
const id = pendingAttachments[i];
const ref = findAttachmentById(id);
if (!ref) continue;
const row = document.createElement('div');
row.className = 'attach-row';
const icon = document.createElement('span');
icon.textContent = ref.kind === 'image' ? '🖼️' : (ref.kind === 'pdf' ? '📄' : (ref.kind === 'zip' ? '📦' : '🧾'));
const name = document.createElement('div');
name.className = 'attach-name';
name.textContent = ref.item.name || (ref.kind + ' attachment');
const meta = document.createElement('span');
meta.className = 'attach-meta';
meta.textContent = bytesToKB(ref.item.size || 0) + ' KB';
const state = document.createElement('span');
const statusValue = String(ref.item.status || 'ready');
state.className = 'attach-state ' + statusValue;
state.textContent = statusValue;
const detail = document.createElement('span');
detail.className = 'attach-detail';
detail.textContent = ref.item.detail || '';
const up = document.createElement('button'); up.className = 'small-btn'; up.textContent = '↑'; up.onclick = () => moveAttachment(id, -1);
const dn = document.createElement('button'); dn.className = 'small-btn'; dn.textContent = '↓'; dn.onclick = () => moveAttachment(id, 1);
const rm = document.createElement('button'); rm.className = 'small-btn'; rm.textContent = '✕'; rm.onclick = () => removeAttachmentById(id);
row.appendChild(icon);
row.appendChild(name);
row.appendChild(meta);
row.appendChild(state);
row.appendChild(detail);
row.appendChild(up);
row.appendChild(dn);
row.appendChild(rm);
attachPanel.appendChild(row);
}
}
// ── Image attachment helpers ──────────────────────────────────────────────
function addPendingImage(base64, mime, dataUrl, fileName) {
if (pendingImages.length >= 5) { setStatus('Max 5 images per message'); return; }
const id = ++attachmentIdCounter;
const size = Math.floor((base64.length * 3) / 4);
pendingImages.push({ id, name: fileName || ('image-' + id), base64, mime, dataUrl, size, status: 'ready', detail: 'Ready to send' });
pendingAttachments.push(id);
renderAttachmentPanel();
updatePayloadMeter();
}
function clearPendingImages() {
pendingImages = [];
pendingPdfFiles = [];
pendingTextFiles = [];
pendingZipFiles = [];
pendingAttachments = [];
if (imagePreviewStrip) {
imagePreviewStrip.innerHTML = '';
imagePreviewStrip.classList.add('empty');
}
renderAttachmentPanel();
updatePayloadMeter();
}
function finalizeAttachmentRead() {
if (pendingAttachmentReads > 0) pendingAttachmentReads -= 1;
if (pendingAttachmentReads === 0 && queuedSend) {
queuedSend = false;
dispatchSend();
}
}
function isZipFile(file) {
const name = String(file?.name || '').toLowerCase();
const type = String(file?.type || '').toLowerCase();
return name.endsWith('.zip') || type === 'application/zip' || type === 'application/x-zip-compressed' || type === 'multipart/x-zip';
}
function isPdfFile(file) {
const name = String(file?.name || '').toLowerCase();
const type = String(file?.type || '').toLowerCase();
return type === 'application/pdf' || name.endsWith('.pdf');
}
function isTextLikeFile(file) {
const name = String(file?.name || '').toLowerCase();
const type = String(file?.type || '').toLowerCase();
if (type.startsWith('text/')) return true;
if (type.includes('json') || type.includes('javascript') || type.includes('typescript') || type.includes('xml') || type.includes('yaml') || type.includes('python') || type.includes('shell')) return true;
return /\.(txt|md|markdown|json|js|jsx|ts|tsx|php|py|rb|go|java|c|cc|cpp|h|hpp|cs|rs|swift|kt|m|mm|scala|sql|html|css|scss|sass|less|xml|yml|yaml|sh|bash|zsh|env|ini|conf|cfg|log|csv)$/i.test(name);
}
function processAttachmentFile(file) {
if (!file) return;
const fileType = String(file.type || '').toLowerCase();
if (isPdfFile(file)) {
if (file.size > 8 * 1024 * 1024) { setStatus('PDF too large (max 8MB)'); return; }
const finishRead = createAttachmentReadGuard('PDF', 15000);
setStatus('Processing PDF...', true);
const r = new FileReader();
r.onload = e => {
const dataUrl = String(e.target.result || '');
if (!dataUrl.includes(',')) { setStatus('Failed to process PDF'); finishRead(); return; }
const base64 = dataUrl.split(',')[1];
const id = ++attachmentIdCounter;
pendingPdfFiles.push({ id, name: file.name || ('file-' + id + '.pdf'), data: base64, size: file.size || 0, status: 'ready', detail: 'Awaiting extraction' });
pendingAttachments.push(id);
renderAttachmentPanel();
updatePayloadMeter();
setStatus('PDF attached');
finishRead();
};
r.onerror = () => { setStatus('Failed to process PDF'); finishRead(); };
r.readAsDataURL(file);
return;
}
if (isZipFile(file)) {
if (file.size > 10 * 1024 * 1024) { setStatus('ZIP too large (max 10MB)'); return; }
const finishRead = createAttachmentReadGuard('ZIP', 15000);
setStatus('Processing ZIP...', true);
const zipReader = new FileReader();
zipReader.onload = e => {
const dataUrl = String(e.target.result || '');
if (!dataUrl.includes(',')) { setStatus('Failed to process ZIP'); finishRead(); return; }
const base64 = dataUrl.split(',')[1];
const id = ++attachmentIdCounter;
pendingZipFiles.push({ id, name: file.name || ('archive-' + id + '.zip'), data: base64, size: file.size || 0, status: 'ready', detail: 'Awaiting extraction' });
pendingAttachments.push(id);
renderAttachmentPanel();
updatePayloadMeter();
setStatus('ZIP attached');
finishRead();
};
zipReader.onerror = () => { setStatus('Failed to process ZIP'); finishRead(); };
zipReader.readAsDataURL(file);
return;
}
if (isTextLikeFile(file)) {
if (file.size > 300 * 1024) {
setStatus('Text file too large (max 300KB)');
return;
}
const finishRead = createAttachmentReadGuard('Text attachment', 12000);
setStatus('Reading text attachment...', true);
const textReader = new FileReader();
textReader.onload = e => {
const content = String(e.target.result || '').slice(0, 12000);
const safeName = String(file.name || 'attachment.txt').split(String.fromCharCode(96)).join('');
const id = ++attachmentIdCounter;
pendingTextFiles.push({ id, name: safeName, text: content, size: file.size || content.length, status: 'ready', detail: 'Ready to send' });
pendingAttachments.push(id);
renderAttachmentPanel();
updatePayloadMeter();
setStatus('Text file attached');
finishRead();
};
textReader.onerror = () => { setStatus('Failed to read text file'); finishRead(); };
textReader.readAsText(file);
return;
}
if (!fileType.startsWith('image/')) {
setStatus('Unsupported file: ' + (file.name || 'attachment'));
return;
}
if (file.size > 5 * 1024 * 1024) { setStatus('Image too large (max 5MB)'); return; }
const finishRead = createAttachmentReadGuard('Image', 12000);
setStatus('Processing image...', true);
const reader = new FileReader();
reader.onload = e => {
const dataUrl = String(e.target.result || '');
if (!dataUrl.includes(',')) { setStatus('Failed to process image'); finishRead(); return; }
const base64 = dataUrl.split(',')[1];
const mime = file.type;
addPendingImage(base64, mime, dataUrl, file.name || 'image');
setStatus('Image attached');
finishRead();
};
reader.onerror = () => { setStatus('Failed to process image'); finishRead(); };
reader.readAsDataURL(file);
}
// Paste handler (Ctrl+V image)
document.addEventListener('paste', e => {
let handled = false;
const items = e.clipboardData?.items;
if (items && items.length) {
for (const item of items) {
if (item && item.kind === 'file') {
const f = item.getAsFile();
if (f) {
e.preventDefault();
processAttachmentFile(f);
handled = true;
}
}
}
}
if (!handled) {
const files = e.clipboardData?.files;
if (files && files.length) {
for (const f of files) {
if (f) {
e.preventDefault();
processAttachmentFile(f);
handled = true;
}
}
}
}
if (handled) setStatus('Attachment added', false);
});
// Drag-and-drop onto chat area
if (chatArea) {
chatArea.addEventListener('dragover', e => { e.preventDefault(); chatArea.style.outline = '2px dashed #e2b340'; });
chatArea.addEventListener('dragleave', () => { chatArea.style.outline = ''; });
chatArea.addEventListener('drop', e => {
e.preventDefault();
chatArea.style.outline = '';
Array.from(e.dataTransfer.files || []).forEach(processAttachmentFile);
});
}
// File picker button
if (attachBtn && fileInput) {
attachBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
Array.from(fileInput.files || []).forEach(processAttachmentFile);
fileInput.value = '';
});
}
if (agentSelect) {
agentSelect.addEventListener('change', () => {
vscode.postMessage({ type: 'set-agent', agent: agentSelect.value });
addMessage('system', 'Switched to ' + agentSelect.options[agentSelect.selectedIndex].text);
});
}
if (modelSelect) {
modelSelect.addEventListener('change', () => {
addMessage('system', 'Model: ' + modelSelect.options[modelSelect.selectedIndex].text);
});
}
// ── MediaRecorder-based recording ──────────────────────────────────────────
async function getMicStream() {
if (mediaStream) return mediaStream;
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true, noiseSuppression: true }
});
return mediaStream;
} catch (e) {
setStatus('Mic access denied: ' + e.message);
return null;
}
}
async function startRecording() {
if (isRecording || isTranscribing) return;
if (isSpeaking) { stopAudio(); }
const stream = await getMicStream();
if (!stream) {
setStatus('No microphone access');
if (handsFreeMode) toggleHandsFree();
return;
}
audioChunks = [];
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus'
: MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm'
: MediaRecorder.isTypeSupported('audio/ogg;codecs=opus') ? 'audio/ogg;codecs=opus'
: 'audio/mp4';
try {
mediaRecorder = new MediaRecorder(stream, { mimeType });
} catch (e) {
mediaRecorder = new MediaRecorder(stream);
}
mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) audioChunks.push(e.data); };
mediaRecorder.onstop = () => { finishRecording(); };
mediaRecorder.start(250);
isRecording = true;
micBtn.classList.add('recording');
micBtn.classList.remove('transcribing');
setStatus('Recording...', true);
silenceTimer = setTimeout(() => {
if (isRecording) stopRecording();
}, 15000);
}
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
clearTimeout(silenceTimer);
isRecording = false;
try { mediaRecorder.stop(); } catch(e) {}
}
async function finishRecording() {
micBtn.classList.remove('recording');
if (audioChunks.length === 0) {
setStatus('No audio captured');
if (handsFreeMode) scheduleHandsFreeRecord();
return;
}
isTranscribing = true;
micBtn.classList.add('transcribing');
setStatus('Transcribing...', true);
const blob = new Blob(audioChunks, { type: mediaRecorder.mimeType || 'audio/webm' });
audioChunks = [];
if (blob.size < 1000) {
isTranscribing = false;
micBtn.classList.remove('transcribing');
setStatus('Too short');
if (handsFreeMode) scheduleHandsFreeRecord();
return;
}
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(',')[1];
const id = ++sttIdCounter;
vscode.postMessage({ type: 'stt-request', audio: base64, mime: blob.type, id });
};
reader.readAsDataURL(blob);
}
function toggleRecording() {
if (isRecording) stopRecording();
else if (isTranscribing) { /* wait */ }
else startRecording();
}
// ── Audio playback ─────────────────────────────────────────────────────────
function stopAudio() {
if (currentAudio) { currentAudio.pause(); currentAudio.currentTime = 0; currentAudio = null; }
isSpeaking = false;
setStatus('Ready');
}
function playAudio(dataUrl) {
if (!voiceEnabled || !dataUrl) return;
stopAudio();
isSpeaking = true;
setStatus('Speaking...', true);
currentAudio = new Audio(dataUrl);
currentAudio.onended = () => {
isSpeaking = false;
currentAudio = null;
setStatus('Ready');
if (handsFreeMode) scheduleHandsFreeRecord();
};
currentAudio.onerror = () => {
isSpeaking = false;
currentAudio = null;
setStatus('Ready');
if (handsFreeMode) scheduleHandsFreeRecord();
};
currentAudio.play().catch(() => {
isSpeaking = false;
setStatus('Ready');
if (handsFreeMode) scheduleHandsFreeRecord();
});
}
// ── Hands-free mode ────────────────────────────────────────────────────────
function toggleHandsFree() {
handsFreeMode = !handsFreeMode;
handsFreeBtn.classList.toggle('active', handsFreeMode);
handsFreeBtn.textContent = handsFreeMode ? 'HF Active' : 'Hands-Free';
if (handsFreeMode) {
addMessage('system', 'Hands-free mode ON — auto-listen after Alfred speaks');
if (!isRecording && !isTranscribing && !isSpeaking) {
scheduleHandsFreeRecord();
}
} else {
addMessage('system', 'Hands-free mode OFF');
if (isRecording) stopRecording();
}
}
function scheduleHandsFreeRecord() {
if (!handsFreeMode || isRecording || isTranscribing || isSpeaking) return;
setTimeout(() => {
if (handsFreeMode && !isRecording && !isTranscribing && !isSpeaking) {
startRecording();
}
}, 800);
}
// ── Message formatting ─────────────────────────────────────────────────────
function formatResponse(text) {
let blockId = 0;
return text
.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
blockId++;
const escapedCode = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const safeLang = (lang || 'text').replace(/[^a-zA-Z0-9]/g, '');
return '<div class="code-block-wrap" data-block-id="cb' + blockId + '">' +
'<div class="code-block-header"><span class="code-lang">' + safeLang + '</span>' +
'<button class="cb-btn cb-copy" onclick="copyCodeBlock(this)" title="Copy to clipboard">Copy</button>' +
'<button class="cb-btn cb-insert" onclick="insertCodeBlock(this)" title="Insert at cursor">Insert</button>' +
((['sh','bash','zsh','shell','cmd','powershell','terminal'].includes(safeLang)) ?
'<button class="cb-btn cb-run" onclick="runCodeBlock(this)" title="Run in terminal">Run</button>' : '') +
'</div>' +
'<pre><code data-code="' + btoa(unescape(encodeURIComponent(code))) + '">' + escapedCode + '</code></pre></div>';
})
.replace(/\`([^\`]+)\`/g, '<code>$1</code>')
.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')
.replace(/\\n/g, '<br>');
}
function formatResponseSafe(raw) {
try {
return formatResponse(raw == null ? '' : String(raw));
} catch (e) {
return String(raw == null ? '' : raw)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\\n/g, '<br>');
}
}
function addMessage(sender, text, isThinking, requestId) {
const div = document.createElement('div');
div.className = 'message ' + (sender === 'commander' ? 'commander' : sender === 'system' ? 'system' : 'alfred');
if (requestId != null && requestId !== '') {
div.setAttribute('data-alfred-req-id', String(requestId));
}
if (sender !== 'system') {
const s = document.createElement('div'); s.className = 'sender';
s.textContent = sender === 'commander' ? 'Commander' : sender.charAt(0).toUpperCase() + sender.slice(1);
div.appendChild(s);
}
const b = document.createElement('div');
b.className = 'msg-content';
if (isThinking) b.innerHTML = '<span class="thinking">Thinking</span>';
else if (sender === 'commander' || sender === 'system') b.textContent = text;
else b.innerHTML = formatResponseSafe(text);
div.appendChild(b);
chatArea.appendChild(div);
chatArea.scrollTop = chatArea.scrollHeight;
return b;
}
function setStatus(text, active) {
if (!statusEl) return;
statusEl.textContent = text;
statusEl.className = 'status' + (active ? ' active' : '');
}
function processInput(text) {
if (!text) return;
const attachmentCount = pendingAttachments.length;
const attachmentNames = pendingAttachments
.map(id => findAttachmentById(id))
.filter(Boolean)
.map(ref => ref.item && ref.item.name ? ref.item.name : ref.kind)
.slice(0, 6);
const contextSuffix = attachmentCount > 0
? ('\\n\\n[Attachments included: ' + attachmentCount + (attachmentNames.length ? (' | ' + attachmentNames.join(', ')) : '') + ']')
: '';
const modelText = text + contextSuffix;
if (pendingAttachments.length > 0) {
const imgHtml = pendingImages.map(p => '<img src="' + p.dataUrl + '" style="max-width:80px;max-height:60px;border-radius:4px;margin:2px;vertical-align:middle;">').join('');
const attachSummary = '<div style="margin-top:4px;font-size:10px;opacity:.8;">Attachments: ' + pendingAttachments.length + '</div>';
const msgDiv = addMessage('commander', text);
msgDiv.parentElement.insertAdjacentHTML('beforeend', '<div style="margin-top:4px;">' + imgHtml + attachSummary + '</div>');
} else {
addMessage('commander', text);
}
if (text.toLowerCase().startsWith('run ')) {
vscode.postMessage({ type: 'run-terminal', command: text.replace(/^run\s+/i, '') });
addMessage('system', 'Sent to terminal — chat not used.');
return;
}
if (text.toLowerCase().startsWith('insert ') || text.toLowerCase().startsWith('type ')) {
vscode.postMessage({ type: 'insert-code', code: text.replace(/^(insert|type)\s+/i, '') });
addMessage('alfred', 'Inserted.'); return;
}
const orderedImages = [];
const orderedPdfs = [];
const orderedTexts = [];
const orderedZips = [];
for (const id of pendingAttachments) {
const ref = findAttachmentById(id);
if (!ref) continue;
if (ref.kind === 'image') orderedImages.push({ data: ref.item.base64, type: ref.item.mime });
if (ref.kind === 'pdf') orderedPdfs.push({ name: ref.item.name, data: ref.item.data });
if (ref.kind === 'text') orderedTexts.push({ name: ref.item.name, text: ref.item.text });
if (ref.kind === 'zip') orderedZips.push({ name: ref.item.name, data: ref.item.data });
}
const outgoingModel = modelSelect ? modelSelect.value : 'sonnet';
const outgoingMultiplier = multiplierSelect ? parseInt(multiplierSelect.value, 10) : 30;
const reqId = ++messageId;
addMessage(agentSelect.value, '', true, reqId);
setStatus(agentSelect.options[agentSelect.selectedIndex].text + ' thinking...', true);
for (const id of pendingAttachments) {
setAttachmentStatusById(id, 'queued', 'Sending to Alfred');
}
renderAttachmentPanel();
lastRequest = { text: modelText, agent: agentSelect.value, model: outgoingModel, multiplier: outgoingMultiplier, images: orderedImages, pdf_files: orderedPdfs, attachment_texts: orderedTexts, zip_files: orderedZips };
logTelemetry('send #' + reqId + ' model=' + outgoingModel + ' multiplier=' + outgoingMultiplier + ' images=' + orderedImages.length + ' pdf=' + orderedPdfs.length + ' text=' + orderedTexts.length + ' zip=' + orderedZips.length);
// PRIMARY: Direct browser fetch to Alfred API (bypasses broken extension host IPC)
alfredDirectChat(reqId, modelText, agentSelect.value, outgoingModel, orderedImages, orderedPdfs, orderedTexts, orderedZips, outgoingMultiplier);
// SECONDARY: Also notify extension host for TTS, editor context, etc.
try { vscode.postMessage({ type: 'ai-request', text: modelText, agent: agentSelect.value, id: reqId, model: outgoingModel, multiplier: outgoingMultiplier, images: orderedImages, pdf_files: orderedPdfs, attachment_texts: orderedTexts, zip_files: orderedZips }); } catch(_) {}
}
// Direct browser-to-API chat (primary path — no extension host IPC needed)
async function alfredDirectChat(reqId, text, agent, model, images, pdfs, texts, zips, multiplier) {
try {
// Request workspace context from extension host for richer payload
vscode.postMessage({ type: 'get-context', id: reqId });
const payload = {
message: text, agent: agent || 'alfred', model: model || 'sonnet',
token_multiplier: multiplier || 30,
channel: 'ide-chat', context: window.__lastWorkspaceContext || '', conv_id: ''
};
// Include auth token injected from extension host
if (window.__alfredToken) payload.ide_session_token = window.__alfredToken;
const ident = window.__alfredIdentity;
if (ident && ident.ide_client_id) {
payload.ide_client_id = ident.ide_client_id;
payload.ide_name = ident.ide_name || '';
payload.ide_ts = ident.ide_ts || 0;
payload.ide_sig = ident.ide_sig || '';
payload.ide_email = ident.ide_email || '';
}
if (images && images.length) payload.images = images;
if (pdfs && pdfs.length) payload.pdf_files = pdfs;
if (texts && texts.length) payload.attachment_texts = texts;
if (zips && zips.length) payload.zip_files = zips;
const hdrs = { 'Content-Type': 'application/json', 'X-Alfred-Source': 'alfred-ide' };
if (window.__alfredToken) {
hdrs['Authorization'] = 'Bearer ' + window.__alfredToken;
hdrs['X-Alfred-IDE-Token'] = window.__alfredToken;
}
if (window.__alfredCsrf) hdrs['X-CSRF-Token'] = window.__alfredCsrf;
logTelemetry('direct-fetch #' + reqId + ' starting...');
let resp = await fetch('https://gositeme.com/api/alfred-chat.php', {
method: 'POST', headers: hdrs, credentials: 'include',
body: JSON.stringify(payload)
});
let data = await parseApiResponse(resp);
// Handle CSRF refresh cycle (first request always returns csrf_token)
if (data.csrf_token) window.__alfredCsrf = data.csrf_token;
let retries = 0;
while (retries < 3 && (data.csrf_refresh || data.response === 'Session initialized. Please retry.' || data.error === 'CSRF validation failed')) {
retries++;
hdrs['X-CSRF-Token'] = window.__alfredCsrf || '';
resp = await fetch('https://gositeme.com/api/alfred-chat.php', {
method: 'POST', headers: hdrs, credentials: 'include',
body: JSON.stringify(payload)
});
data = await parseApiResponse(resp);
if (data.csrf_token) window.__alfredCsrf = data.csrf_token;
}
const responseText = data.response || data.message || data.error || 'No response from AI';
if (data.identity) applyIdentity(data.identity);
if (data.attachment_report) applyAttachmentReport(data.attachment_report);
if (data.conv_id) payload.conv_id = data.conv_id;
const row = chatArea.querySelector('.message[data-alfred-req-id="' + reqId + '"]');
const p = row ? row.querySelector('.msg-content') : null;
if (p) {
p.innerHTML = formatResponseSafe(responseText);
const s = row.querySelector('.sender');
if (s && data.agent) s.textContent = data.agent.charAt(0).toUpperCase() + data.agent.slice(1);
const retry = document.createElement('button');
retry.className = 'small-btn retry-btn'; retry.textContent = 'Retry';
retry.onclick = () => { if (!lastRequest) return; const rid = ++messageId; addMessage(lastRequest.agent || 'alfred', '', true, rid); alfredDirectChat(rid, lastRequest.text, lastRequest.agent, lastRequest.model, lastRequest.images, lastRequest.pdf_files, lastRequest.attachment_texts, lastRequest.zip_files, lastRequest.multiplier || 30); };
p.appendChild(retry);
} else {
addMessage(data.agent || agent || 'alfred', responseText);
}
setStatus('Ready');
logTelemetry('direct-fetch ok #' + reqId);
} catch (err) {
const row = chatArea.querySelector('.message[data-alfred-req-id="' + reqId + '"]');
const p = row ? row.querySelector('.msg-content') : null;
const msg = normalizeApiError(err);
if (p) p.innerHTML = msg;
else addMessage('alfred', msg);
setStatus('Error — try again');
logTelemetry('direct-fetch error: ' + (err.message || 'unknown'));
}
}
async function parseApiResponse(resp) {
const raw = await resp.text();
if (!raw || !raw.trim()) {
throw new Error('empty-response');
}
try {
return JSON.parse(raw);
} catch (_) {
return { response: raw.trim() };
}
}
function normalizeApiError(err) {
const message = String((err && err.message) || 'unknown');
if (message === 'empty-response') {
return 'Alfred returned an empty response. Try again.';
}
if (/JSON\.parse|unexpected end of data|Unexpected end of JSON/i.test(message)) {
return 'Alfred returned an invalid response. Try again.';
}
return 'API error: ' + message + ' — check network or try again.';
}
// ── Message handler from extension host ────────────────────────────────────
window.addEventListener('message', (event) => {
const msg = event.data;
if (msg.type === 'ai-response') {
// Extension host responded — direct fetch is primary, so only use this for TTS/supplemental
// If the direct fetch did not fill the response, use the extension-host reply.
logTelemetry('ext-host-response #' + (msg.id || '?') + ' received (direct fetch is primary)');
if (msg.identity) applyIdentity(msg.identity);
if (msg.attachment_report) applyAttachmentReport(msg.attachment_report);
const row = chatArea.querySelector('.message[data-alfred-req-id="' + (msg.id || '') + '"]');
const p = row ? row.querySelector('.msg-content') : null;
if (p && (/thinking/i.test(p.textContent || '') || /API error:/i.test(p.textContent || '') || /empty response/i.test(p.textContent || ''))) {
p.innerHTML = formatResponseSafe(msg.text || 'No response from AI');
const s = row.querySelector('.sender');
if (s && msg.agent) s.textContent = msg.agent.charAt(0).toUpperCase() + msg.agent.slice(1);
setStatus('Ready');
}
} else if (msg.type === 'play-audio') {
playAudio(msg.audio);
} else if (msg.type === 'stt-result') {
isTranscribing = false;
micBtn.classList.remove('transcribing');
const text = (msg.text || '').trim();
if (text) {
setStatus('Heard: ' + text.substring(0, 40) + (text.length > 40 ? '...' : ''));
processInput(text);
} else {
setStatus(msg.error || 'No speech detected');
if (handsFreeMode) scheduleHandsFreeRecord();
}
} else if (msg.type === 'command-result') {
if (msg.text) { addMessage('alfred', msg.text); vscode.postMessage({ type: 'tts-request', text: msg.text }); }
} else if (msg.type === 'toggle-listening') { toggleRecording(); }
else if (msg.type === 'run-terminal-cmd') { vscode.postMessage({ type: 'run-terminal', command: msg.command }); }
else if (msg.type === 'user-profile') {
applyIdentity(msg.profile || {});
}
else if (msg.type === 'workspace-context') {
// Cache latest workspace context for direct API calls
window.__lastWorkspaceContext = msg.context || '';
}
else if (msg.type === 'active-file-changed') {
// Update status with current file info
if (msg.file && msg.file.file) {
const statusHint = msg.file.file + ' (' + msg.file.language + ', L' + msg.file.cursorLine + ')';
logTelemetry('active-file: ' + statusHint);
}
}
else if (msg.type === 'file-saved') {
logTelemetry('saved: ' + (msg.file || 'unknown'));
}
else if (msg.type === 'file-content') {
// File content received — add as system message
if (msg.content) {
addMessage('system', 'File: ' + (msg.filePath || '') + '\\n' + msg.content.substring(0, 2000));
}
}
else if (msg.type === 'search-results') {
if (msg.files && msg.files.length > 0) {
addMessage('system', 'Found ' + msg.files.length + ' files matching "' + (msg.pattern || '') + '":\\n' + msg.files.join('\\n'));
} else {
addMessage('system', 'No files found matching "' + (msg.pattern || '') + '"');
}
}
else if (msg.type === 'grep-results') {
if (msg.results) {
addMessage('system', 'Grep results for "' + (msg.text || '') + '":\\n' + msg.results.substring(0, 3000));
}
}
else if (msg.type === 'git-info') {
if (msg.info) {
const g = msg.info;
addMessage('system', 'Git: branch=' + g.branch + ', last commit: ' + g.lastCommit + (g.dirty ? ', changes: ' + g.dirty : '') + (g.remoteUrl ? ', remote: ' + g.remoteUrl : ''));
}
}
else if (msg.type === 'diagnostics-info') {
if (msg.info) {
addMessage('system', 'Diagnostics: ' + msg.info.errors + ' errors, ' + msg.info.warnings + ' warnings' + (msg.info.errorFiles.length ? ' in: ' + msg.info.errorFiles.join(', ') : ''));
}
}
else if (msg.type === 'project-structure') {
if (msg.structure) {
addMessage('system', 'Project structure:\\n' + msg.structure.substring(0, 3000));
}
}
});
// ── Event listeners ────────────────────────────────────────────────────────
if (micBtn) micBtn.addEventListener('click', toggleRecording);
const voiceToggleBtn = document.getElementById('voiceToggle');
function bindPanelButton(el, handler) {
if (!el) return;
// Capture phase makes these controls resilient when host UI adds bubbling handlers.
el.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
try { handler(); } catch (err) { setStatus('Control error: ' + (err && err.message ? err.message : String(err))); }
}, true);
}
bindPanelButton(voiceToggleBtn, () => {
voiceEnabled = !voiceEnabled;
voiceToggleBtn.textContent = voiceEnabled ? 'Voice ON' : 'Voice OFF';
voiceToggleBtn.classList.toggle('off', !voiceEnabled);
if (!voiceEnabled) stopAudio();
setStatus('Voice ' + (voiceEnabled ? 'enabled' : 'disabled'));
addMessage('system', 'Voice ' + (voiceEnabled ? 'enabled' : 'disabled'));
});
bindPanelButton(handsFreeBtn, () => toggleHandsFree());
function dispatchSend(ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation();
}
if (pendingAttachmentReads > 0) {
queuedSend = true;
setStatus('Finishing attachment processing...', true);
return;
}
const t = textInput.value.trim();
if (!t && pendingAttachments.length === 0) {
setStatus('Type a message or attach a file');
return;
}
textInput.value = '';
const msg = t || '(See attached file)';
processInput(msg);
clearPendingImages();
updatePayloadMeter();
}
function bindSend() {
if (!sendBtn) return;
// Capture phase — runs before bubble handlers so nothing can hijack Send for navigation.
sendBtn.addEventListener('click', dispatchSend, true);
}
bindSend();
// Backup: if DOM wasn't ready, retry on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { bindSend(); });
}
(function bindIdeQuick() {
[['acIdeTerminal','terminal'], ['acIdeSave','save'], ['acIdeSaveAll','saveAll'], ['acIdePalette','palette'], ['acIdeSplit','split'], ['acIdeNew','newFile'], ['acIdeGit','git'], ['acIdeProblems','problems'], ['acIdeSearch','search'], ['acIdeFormat','format']].forEach(([id, cmd]) => {
const el = document.getElementById(id);
if (el) el.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); vscode.postMessage({ type: 'ide-quick', cmd }); });
});
})();
if (textInput) {
textInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
dispatchSend(e);
}
});
}
window.addEventListener('error', (ev) => {
try {
const msg = (ev && ev.message) ? ev.message : 'Unknown UI error';
setStatus('UI error: ' + msg);
addMessage('system', '[ERROR] ' + msg);
logTelemetry('ui-error: ' + msg);
} catch (_) {}
});
window.addEventListener('unhandledrejection', (ev) => {
try {
const msg = (ev.reason && ev.reason.message) ? ev.reason.message : String(ev.reason || 'Unknown promise rejection');
setStatus('Promise error: ' + msg);
addMessage('system', '[PROMISE ERROR] ' + msg);
logTelemetry('promise-error: ' + msg);
} catch (_) {}
});
// ── Code block action buttons ────────────────────────────────────────────
function copyCodeBlock(btn) {
try {
const wrap = btn.closest('.code-block-wrap');
const codeEl = wrap.querySelector('code[data-code]');
const code = decodeURIComponent(escape(atob(codeEl.getAttribute('data-code'))));
navigator.clipboard.writeText(code).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
}).catch(() => {
// Fallback for no clipboard API
const ta = document.createElement('textarea');
ta.value = code;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
});
} catch(e) { setStatus('Copy failed: ' + e.message); }
}
function insertCodeBlock(btn) {
try {
const wrap = btn.closest('.code-block-wrap');
const codeEl = wrap.querySelector('code[data-code]');
const code = decodeURIComponent(escape(atob(codeEl.getAttribute('data-code'))));
vscode.postMessage({ type: 'insert-code', code: code });
btn.textContent = 'Inserted!';
setTimeout(() => { btn.textContent = 'Insert'; }, 1500);
} catch(e) { setStatus('Insert failed: ' + e.message); }
}
function runCodeBlock(btn) {
try {
const wrap = btn.closest('.code-block-wrap');
const codeEl = wrap.querySelector('code[data-code]');
const code = decodeURIComponent(escape(atob(codeEl.getAttribute('data-code'))));
vscode.postMessage({ type: 'run-terminal', command: code.trim() });
btn.textContent = 'Sent!';
setTimeout(() => { btn.textContent = 'Run'; }, 1500);
addMessage('system', 'Command sent to terminal.');
} catch(e) { setStatus('Run failed: ' + e.message); }
}
// ── Commander identity greeting ──────────────────────────────────────────
(function commanderGreeting() {
const ident = window.__alfredIdentity || {};
const greetEl = document.getElementById('greetingMsg');
if (!greetEl) return;
const contentEl = greetEl.querySelector('div:not(.sender)');
if (!contentEl) return;
const isCommander = ident.ide_client_id === 33;
const name = ident.ide_name || 'there';
if (isCommander) {
contentEl.innerHTML = '<strong>Commander on deck.</strong> All systems nominal.<br>' +
'<span style="color:#e2b340;">Full workspace intelligence active. Voice, code actions, file ops, git — everything is live.</span><br>' +
'<span style="font-size:10px;color:#8b949e;">Sonnet 4.6 default · ' + new Date().toLocaleDateString('en-US', {weekday:'long', month:'long', day:'numeric'}) + ' · GoForge synced</span>';
} else if (name && name !== 'there') {
contentEl.innerHTML = 'Welcome, <strong>' + name + '</strong>. Alfred IDE is ready.<br>' +
'<span style="font-size:10px;color:#8b949e;">Type a question, paste code, attach files, or use voice. I have full context of your workspace.</span>';
} else {
contentEl.innerHTML = 'Alfred IDE ready. Full AI coding assistant at your service.<br>' +
'<span style="font-size:10px;color:#8b949e;">Speak or type. Sonnet 4.6 connected.</span>';
}
})();
setStatus('Ready');
</script>
</body>
</html>`;
}
function deactivate() {}
function getWorkspaceStatusHTML(token) {
return `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<style>
body{background:#0e0e1a;color:#e0e0e8;font-family:'Segoe UI',system-ui,sans-serif;padding:24px;margin:0}
h1{color:#00D4FF;font-size:22px;margin:0 0 20px}
.card{background:#16162a;border:1px solid #2a2a4a;border-radius:12px;padding:16px;margin-bottom:16px}
.card h2{color:#00D4FF;font-size:16px;margin:0 0 12px;display:flex;align-items:center;gap:8px}
.row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #1a1a30}
.row:last-child{border:none}
.label{color:#8888aa;font-size:13px}
.val{color:#fff;font-weight:600;font-size:13px}
.ok{color:#10b981}.warn{color:#f59e0b}.bad{color:#ef4444}
.bar{height:8px;border-radius:4px;background:#1a1a30;margin-top:4px}
.bar-fill{height:100%;border-radius:4px;transition:width .5s}
.btn{background:linear-gradient(135deg,#00a8ff,#00d4ff);color:#000;border:none;padding:10px 20px;border-radius:8px;cursor:pointer;font-weight:700;font-size:13px;margin-top:12px}
.btn:hover{opacity:.9}
.btn.danger{background:linear-gradient(135deg,#ff4444,#ff6666)}
.loading{color:#8888aa;font-style:italic}
#error{color:#ef4444;margin:12px 0;display:none}
</style>
</head><body>
<h1>🖥 Workspace Status</h1>
<div id="error"></div>
<div id="content"><p class="loading">Loading workspace data...</p></div>
<script>
const token = ${JSON.stringify(token)};
async function load() {
try {
const r = await fetch('https://gositeme.com/api/alfred-ide-workspace.php?action=status', {
headers: token ? {'Authorization': 'Bearer ' + token} : {},
credentials: 'include'
});
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
render(d);
} catch(e) {
document.getElementById('error').style.display = 'block';
document.getElementById('error').textContent = 'Failed to load: ' + e.message;
document.getElementById('content').innerHTML = '';
}
}
function render(d) {
const ws = d.workspace || {};
const sv = d.server || {};
const ide = d.ide || {};
const pm2 = d.pm2 || null;
const bk = d.backup || null;
const diskPct = sv.disk_used_pct || 0;
const diskColor = diskPct > 80 ? '#ef4444' : diskPct > 60 ? '#f59e0b' : '#10b981';
const memPct = sv.mem_total_gb ? Math.round((1 - sv.mem_avail_gb/sv.mem_total_gb)*100) : 0;
const memColor = memPct > 80 ? '#ef4444' : memPct > 60 ? '#f59e0b' : '#10b981';
const ideStatus = ide.status === 'online' ? '<span class="ok">● Online</span>' : '<span class="bad">● ' + (ide.status||'unknown') + '</span>';
let html = '<div class="card"><h2>👤 Session</h2>';
html += row('User', ws.user);
html += row('Role', ws.role === 'commander' ? '⭐ Commander' : 'Customer');
html += row('Session Expires', ws.session_expires ? new Date(ws.session_expires).toLocaleString() : 'N/A');
html += '</div>';
html += '<div class="card"><h2>⚡ IDE Service</h2>';
html += row('Status', ideStatus);
html += row('Memory', ide.memory_mb ? ide.memory_mb + ' MB' : 'N/A');
html += row('CPU', ide.cpu != null ? ide.cpu + '%' : 'N/A');
html += row('Restarts', ide.restarts != null ? ide.restarts : 'N/A');
html += row('PID', ide.pid || 'N/A');
html += '</div>';
html += '<div class="card"><h2>💾 Server</h2>';
html += row('Disk', sv.disk_used_pct + '% used (' + (sv.disk_total_gb - sv.disk_free_gb).toFixed(0) + ' / ' + sv.disk_total_gb + ' GB)');
html += '<div class="bar"><div class="bar-fill" style="width:'+diskPct+'%;background:'+diskColor+'"></div></div>';
html += row('Memory', memPct + '% used (' + (sv.mem_total_gb - sv.mem_avail_gb).toFixed(1) + ' / ' + sv.mem_total_gb + ' GB)');
html += '<div class="bar"><div class="bar-fill" style="width:'+memPct+'%;background:'+memColor+'"></div></div>';
html += row('Load', sv.load_1m + ' / ' + sv.load_5m);
html += row('Uptime', sv.uptime_days + ' days');
html += '</div>';
if (pm2) {
html += '<div class="card"><h2>🔧 PM2 Services</h2>';
html += row('Online', '<span class="ok">' + pm2.online + '</span>');
html += row('Stopped', pm2.stopped > 0 ? '<span class="warn">' + pm2.stopped + '</span>' : '0');
html += row('Errored', pm2.errored > 0 ? '<span class="bad">' + pm2.errored + '</span>' : '0');
if (pm2.critical) {
html += '<div style="margin-top:8px;font-size:12px;color:#8888aa">Critical Services:</div>';
for (const [n, s] of Object.entries(pm2.critical)) {
const cls = s === 'online' ? 'ok' : 'bad';
html += '<div class="row"><span class="label">' + n + '</span><span class="val ' + cls + '">' + s + '</span></div>';
}
}
html += '</div>';
}
if (bk) {
html += '<div class="card"><h2>📦 Backup</h2>';
html += row('Last Success', bk.last_success || 'Never');
html += row('Age', bk.age_hours != null ? bk.age_hours + ' hours' : 'N/A');
html += row('Health', bk.healthy ? '<span class="ok">✓ Healthy</span>' : '<span class="bad">✗ Stale (&gt;48h)</span>');
html += '</div>';
}
if (ws.role === 'commander') {
html += '<button class="btn danger" onclick="restartIDE()">⟳ Restart IDE Service</button>';
}
document.getElementById('content').innerHTML = html;
}
function row(l, v) { return '<div class="row"><span class="label">'+l+'</span><span class="val">'+v+'</span></div>'; }
async function restartIDE() {
if (!confirm('Restart Alfred IDE service?')) return;
try {
const r = await fetch('https://gositeme.com/api/alfred-ide-workspace.php?action=restart', {
method: 'POST',
headers: token ? {'Authorization': 'Bearer ' + token} : {},
credentials: 'include'
});
const d = await r.json();
alert(d.success ? 'IDE restarted successfully' : 'Restart may have failed: ' + (d.output||''));
} catch(e) { alert('Error: ' + e.message); }
}
load();
setInterval(load, 30000);
</script>
</body></html>`;
}
module.exports = { activate, deactivate };