v1.1.0: Full workspace intelligence engine, Commander identity system, code block actions (copy/insert/run), rich context injection, GoForge-only, purged GitHub refs
- Workspace Intelligence: project detection (20+ types), file tree, git info, diagnostics, open editors, terminal names - Commander System Prompt: knows Danny (client_id 33), ecosystem awareness, coding excellence directives - Rich Context: every API call now includes active file, cursor pos, selected code, project type, git branch, diagnostics - Code Block Actions: Copy, Insert at cursor, Run in terminal — buttons on every code block response - New IDE Quick Actions: Git, Problems, Search, Format (added to existing Terminal, Save, Commands, Split, New) - File Operations: read-file, search-files, grep-search, open-file, apply-edit, create-file handlers - Live Context Push: editor changes and file saves pushed to webview in real-time - Commander Greeting: personalized startup message when Danny logs in - GoForge Only: repository + homepage point to alfredlinux.com/forge, removed GitHub Copilot refs from extensions.json - 2093 → 2686 lines
This commit is contained in:
parent
fd7361e044
commit
d32f46dc59
631
extension.js
631
extension.js
@ -3,7 +3,9 @@ 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;
|
||||
@ -12,6 +14,336 @@ 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 getProjectLanguages() {
|
||||
const root = getWorkspaceRoot();
|
||||
const exts = safeExec('find . -maxdepth 4 -type f -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.venv/*" -not -path "./vendor/*" | sed "s/.*\\.//" | sort | uniq -c | sort -rn | head -20', root);
|
||||
return exts || '';
|
||||
}
|
||||
|
||||
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 || '';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 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
|
||||
|
||||
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
|
||||
|
||||
`;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const IDE_SESSION_BRIDGES = [
|
||||
'/home/gositeme/domains/gositeme.com/logs/alfred-ide/session.json',
|
||||
'/home/gositeme/.alfred-ide/session.json'
|
||||
@ -292,6 +624,20 @@ class AlfredCommanderProvider {
|
||||
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);
|
||||
@ -320,6 +666,95 @@ class AlfredCommanderProvider {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
@ -328,6 +763,23 @@ class AlfredCommanderProvider {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -425,27 +877,17 @@ function generateTTS(text) {
|
||||
}
|
||||
|
||||
function getEditorContext() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return '';
|
||||
const doc = editor.document;
|
||||
const sel = editor.selection;
|
||||
let ctx = `[IDE Context] File: ${doc.fileName}, Language: ${doc.languageId}, Lines: ${doc.lineCount}`;
|
||||
if (!sel.isEmpty) {
|
||||
const selected = doc.getText(sel).substring(0, 500);
|
||||
ctx += `, Selected code:\n\`\`\`${doc.languageId}\n${selected}\n\`\`\``;
|
||||
} else {
|
||||
const line = doc.lineAt(sel.active.line);
|
||||
ctx += `, Current line ${sel.active.line + 1}: ${line.text.substring(0, 200)}`;
|
||||
}
|
||||
return ctx;
|
||||
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
|
||||
token_multiplier: multiplier || 30,
|
||||
system_prompt: systemPrompt
|
||||
};
|
||||
const ideToken = readLocalIdeToken();
|
||||
const ideIdentity = buildIdeIdentityPayload();
|
||||
@ -869,6 +1311,16 @@ function getWebviewContent(injectedToken, injectedIdentity) {
|
||||
.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>
|
||||
@ -950,11 +1402,15 @@ function getWebviewContent(injectedToken, injectedIdentity) {
|
||||
<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">
|
||||
<div class="message alfred" id="greetingMsg">
|
||||
<div class="sender">Alfred</div>
|
||||
<div>Ready. Claude Sonnet 4 connected. Speak or type.</div>
|
||||
<div>Initializing workspace intelligence...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attach-panel empty" id="attachPanel"></div>
|
||||
@ -1616,8 +2072,21 @@ function scheduleHandsFreeRecord() {
|
||||
// ── Message formatting ─────────────────────────────────────────────────────
|
||||
|
||||
function formatResponse(text) {
|
||||
let blockId = 0;
|
||||
return text
|
||||
.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, '<pre><code>$2</code></pre>')
|
||||
.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
|
||||
blockId++;
|
||||
const escapedCode = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
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>');
|
||||
@ -1724,10 +2193,13 @@ function processInput(text) {
|
||||
// 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: '', conv_id: ''
|
||||
channel: 'ide-chat', context: window.__lastWorkspaceContext || '', conv_id: ''
|
||||
};
|
||||
// Include auth token injected from extension host
|
||||
if (window.__alfredToken) payload.ide_session_token = window.__alfredToken;
|
||||
@ -1864,6 +2336,54 @@ window.addEventListener('message', (event) => {
|
||||
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 ────────────────────────────────────────────────────────
|
||||
@ -1926,7 +2446,7 @@ if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() { bindSend(); });
|
||||
}
|
||||
(function bindIdeQuick() {
|
||||
[['acIdeTerminal','terminal'], ['acIdeSave','save'], ['acIdeSaveAll','saveAll'], ['acIdePalette','palette'], ['acIdeSplit','split'], ['acIdeNew','newFile']].forEach(([id, cmd]) => {
|
||||
[['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 }); });
|
||||
});
|
||||
@ -1956,6 +2476,79 @@ window.addEventListener('unhandledrejection', (ev) => {
|
||||
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>
|
||||
|
||||
@ -5,8 +5,6 @@
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode-remote.remote-ssh",
|
||||
"ms-vscode.remote-server",
|
||||
"ms-vscode.remote-explorer",
|
||||
"github.copilot",
|
||||
"github.copilot-chat"
|
||||
"ms-vscode.remote-explorer"
|
||||
]
|
||||
}
|
||||
|
||||
@ -4,6 +4,11 @@
|
||||
"description": "Fresh Alfred panel: all agents & models, voice STT/TTS, attachments, hands-free. IDE shortcuts are explicit buttons — chat text always goes to AI.",
|
||||
"version": "1.0.1",
|
||||
"publisher": "gositeme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://alfredlinux.com/forge/commander/alfred-ide.git"
|
||||
},
|
||||
"homepage": "https://alfredlinux.com/forge/commander/alfred-ide",
|
||||
"engines": {
|
||||
"vscode": "^1.70.0"
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user