diff --git a/README.md b/README.md index c7e37ed..4652dde 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ This is for if you want to enhance the extension or fix bugs. > [!TIP] > You don't usually need to run this from source unless you are working on this extension. -Open this repo via `code .` and then press F5 to run a new workspace with this in it. +Open this repo via `code .`, open `src/extension.ts` and then press F5 to run a new workspace with this in it. ### Packaging the Extension diff --git a/package.json b/package.json index 7b854db..22c8235 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,15 @@ "license": "Apache-2.0", "description": "This is a vscode extension for goose: a programming agent that runs on your machine. Consider this extension experimental at this stage. You need goose-ai package installed for this to work. https://github.com/block-open-source/goose", "devDependencies": { + "@types/js-yaml": "^4.0.9", "@types/node": "^22.3.0", "@types/vscode": "^1.91.0", "typescript": "^5.5.4", "vsce": "^2.15.0" }, "dependencies": { - "@vscode/l10n": "^0.0.18" + "@vscode/l10n": "^0.0.18", + "js-yaml": "^4.1.0" }, "engines": { "vscode": "^1.91.1" @@ -34,6 +36,29 @@ "*" ], "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "profileManager", + "title": "Profile Manager", + "icon": "goose-icon.png" + } + ] + }, + "views": { + "profileManager": [ + { + "type": "webview", + "id": "profileFormView", + "name": "Profile Form" + }, + { + "type": "tree", + "id": "profileExplorer", + "name": "Profiles" + } + ] + }, "configuration": { "type": "object", "title": "Goose Settings", @@ -58,6 +83,26 @@ { "command": "extension.openGoose", "title": "🪿 open goose 🪿" + }, + { + "command": "extension.refreshProfiles", + "title": "🪿 Refresh Profiles" + }, + { + "command": "extension.addProfile", + "title": "🪿 Add Profile" + }, + { + "command": "profileExplorer.select", + "title": "Select Profile" + }, + { + "command": "extension.updateProfile", + "title": "Update Profile" + }, + { + "command": "extension.deleteProfile", + "title": "🪿 Delete Profile" } ], "menus": { @@ -67,6 +112,30 @@ "when": "editorHasSelection", "group": "navigation" } + ], + "view/title": [ + { + "command": "extension.refreshProfiles", + "when": "view == profileExplorer", + "group": "navigation" + }, + { + "command": "extension.addProfile", + "when": "view == profileExplorer", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "profileExplorer.select", + "when": "view == profileExplorer", + "group": "inline" + }, + { + "command": "extension.deleteProfile", + "when": "view == profileExplorer", + "group": "inline" + } ] } } diff --git a/src/extension.ts b/src/extension.ts index c644f9a..9b80994 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,146 +2,258 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import * as yaml from 'js-yaml'; import { execSync } from 'child_process'; let gooseTerminal: vscode.Terminal | undefined; const terminalName = '\u2728 goose chat \u2728'; -export function activate(context: vscode.ExtensionContext) { +// Profiles interface +interface Profiles { + [key: string]: { + provider: string; + processor: string; + accelerator: string; + moderator: string; + toolkits?: Array<{ + name: string; + requires: Record; + }>; + } +} - // Check if goose CLI is installed - const config = vscode.workspace.getConfiguration('goose'); - let defaultCommand = config.get('defaultCommand', "goose session start"); +// Webview View Provider +class ProfileFormViewProvider implements vscode.WebviewViewProvider { + private _view?: vscode.WebviewView; + private selectedProfileName: string = ''; + private profiles: Profiles; - if (defaultCommand.startsWith('goose ')) { - try { - execSync('sq goose version'); - defaultCommand = 'sq goose session start' - console.log("using sq: " + defaultCommand) - } catch (error) { - console.log("falling back to default " + defaultCommand) - } - + constructor(private readonly context: vscode.ExtensionContext, profiles: Profiles) { + this.profiles = profiles; } - - - let getTerminal = () => { - if (!gooseTerminal || gooseTerminal.exitStatus !== undefined) { - vscode.window.showInformationMessage('goose agent starting, this may take a minute.. ⏰'); - gooseTerminal = vscode.window.createTerminal({ - name: terminalName, - message: 'Loading Goose Session...', // Add a message to make it clear what terminal is for - }); - gooseTerminal.sendText(defaultCommand); + public setProfileName(profileName: string) { + this.selectedProfileName = profileName; + if (this._view) { + this._view.webview.html = this.getHtmlForWebview(); } + } - console.log('Goose terminal created:', gooseTerminal.name); - gooseTerminal.show(); // Delayed terminal show - - return gooseTerminal + resolveWebviewView(webviewView: vscode.WebviewView) { + this._view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + }; + webviewView.webview.onDidReceiveMessage(async message => { + if (message.command === 'updateProfile') { + await vscode.commands.executeCommand('extension.updateProfile', undefined, message); + } + }); + + webviewView.webview.html = this.getHtmlForWebview(); } - let openGooseTerminal = vscode.commands.registerCommand('extension.openGoose', () => { - getTerminal(); - }); - context.subscriptions.push(openGooseTerminal); + private getHtmlForWebview() { + const profile = this.profiles[this.selectedProfileName] || { + provider: '', + processor: '', + accelerator: '', + moderator: '', + toolkits: [] + }; - let openTerminalDisposable = vscode.commands.registerCommand('extension.openGooseTerminal', () => { - getTerminal(); - }); - context.subscriptions.push(openTerminalDisposable); - - // Automatically open the terminal when the extension activates - //vscode.commands.executeCommand('extension.openGooseTerminal'); - - - function createTempFileWithLines(selectedText: string, startLine: number): string { - const selectedLines = selectedText.split('\n').map((line, index) => `${startLine + index}: ${line}`).join('\n'); - const tempDir = os.tmpdir(); - const tempFileName = path.join(tempDir, `goose_context_${Date.now()}.txt`); - fs.writeFileSync(tempFileName, selectedLines); - return tempFileName; + return ` + + + + + + Profile Form + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + + `; + } } -let sendToGooseDisposable = vscode.commands.registerCommand('extension.sendToGoose', async () => { - const editor = vscode.window.activeTextEditor; - if (!editor) { - return; - } +// Profile Data Provider +class ProfileProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - // Get the selected text - const selection = editor.selection; + constructor(private profiles: Profiles) {} - // Get the file path and selection range - const filePath = editor.document.uri.fsPath; - const startLine = selection.start.line + 1; // Line numbers are 1-based for better readability - const endLine = selection.end.line + 1; + getTreeItem(element: ProfileItem): vscode.TreeItem { + return element; + } - // Prompt the user for a question - const question = await vscode.window.showInputBox({ prompt: 'Ask goose something:' }); - if (!question) { - return; + getChildren(element?: ProfileItem): Thenable { + if (!element) { + return Promise.resolve(Object.keys(this.profiles).map(name => new ProfileItem(name, vscode.TreeItemCollapsibleState.None))); } - - const selectedText = editor.document.getText(selection); - const hasSelectedText = selectedText.trim().length > 0; - let textToAskGoose = question; - if (hasSelectedText) { - // There is some selected text - const tempFileName = createTempFileWithLines(selectedText, startLine); - textToAskGoose = `Looking at file: ${filePath} with context: ${tempFileName}.` + - ` please load the file, answer this question: [${question}].` + - ` Note: If editing is required, keep edits around these lines and don't delete or modify unrelated code.` - } else { - // cursor is just position in file - const cursorLine = editor.selection.active.line + 1; - textToAskGoose = `Looking at file: ${filePath}, you are on line ${cursorLine}. ` + - `Please answer the query: [${question}]` + return Promise.resolve([]); + } - } - editor.document.save(); - getTerminal().sendText(textToAskGoose); - - // Check config for opening diff editor after Goose edits - const openDiffAfterEdit = config.get('openDiffEditorAfterGooseEdits', false); - if (openDiffAfterEdit) { - // Watch for changes in the active text editor - const watcher = vscode.workspace.createFileSystemWatcher(filePath); - watcher.onDidChange(() => { - vscode.commands.executeCommand('workbench.view.scm'); - watcher.dispose(); // Stop watching after opening SCM view - }); - } - }); - - context.subscriptions.push(sendToGooseDisposable); + refresh(): void { + this._onDidChangeTreeData.fire(); + } +} + +class ProfileItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + ) { + super(label, collapsibleState); + this.contextValue = 'profileItem'; + this.command = { command: 'profileExplorer.select', title: "Select Profile", arguments: [this.label] }; + } +} +export function activate(context: vscode.ExtensionContext) { + const filePath = path.join(os.homedir(), '.config', 'goose', 'profiles.yaml'); + const profiles: Profiles = yaml.load(fs.readFileSync(filePath, 'utf8')) as Profiles || {} as Profiles; + const profileProvider = new ProfileProvider(profiles); + const profileFormProvider = new ProfileFormViewProvider(context, profiles); + vscode.window.registerTreeDataProvider('profileExplorer', profileProvider); + vscode.window.registerWebviewViewProvider('profileFormView', profileFormProvider); + const selectProfileCommand = vscode.commands.registerCommand('profileExplorer.select', (profileName: string) => { + profileFormProvider.setProfileName(profileName); + vscode.commands.executeCommand('profileFormView.focus'); + }); + const updateProfileCommand = vscode.commands.registerCommand('extension.updateProfile', (_, message) => { + profiles[message.profile] = message.data; + try { + fs.writeFileSync(filePath, yaml.dump(profiles)); + profileProvider.refresh(); + vscode.window.showInformationMessage(`Profile ${message.profile} updated successfully.`); + } catch (error) { + vscode.window.showErrorMessage(`Failed to update profile: ${error}`); + } + }); + const addProfileCommand = vscode.commands.registerCommand('extension.addProfile', async () => { + const profileName = await vscode.window.showInputBox({ + placeHolder: 'Enter new profile name' + }); + + if (profileName) { + profiles[profileName] = { + provider: '', + processor: '', + accelerator: '', + moderator: '', + toolkits: [] + }; + profileProvider.refresh(); + profileFormProvider.setProfileName(profileName); + } + }); - // Completion suggestion: ask Goose to finish it - vscode.languages.registerCodeActionsProvider('*', { - provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken) { - const codeAction = new vscode.CodeAction('Ask goose to fix it', vscode.CodeActionKind.QuickFix); - codeAction.command = { command: 'extension.askGooseToFix', title: 'Ask goose to fix it' }; - return [codeAction]; + const deleteProfileCommand = vscode.commands.registerCommand('extension.deleteProfile', async (item: ProfileItem) => { + const profileName = item.label; + const answer = await vscode.window.showWarningMessage( + `Are you sure you want to delete profile "${profileName}"?`, + 'Yes', + 'No' + ); + + if (answer === 'Yes') { + delete profiles[profileName]; + fs.writeFileSync(filePath, yaml.dump(profiles)); + profileProvider.refresh(); + vscode.window.showInformationMessage(`Profile ${profileName} deleted.`); } }); - // Completion suggestion: ask goose (general) - vscode.languages.registerCodeActionsProvider('*', { - provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken) { - const codeAction = new vscode.CodeAction('Ask goose', vscode.CodeActionKind.QuickFix); - codeAction.command = { command: 'extension.sendToGoose', title: 'Ask goose' }; - return [codeAction]; + context.subscriptions.push(selectProfileCommand, updateProfileCommand, addProfileCommand, deleteProfileCommand); + + // Existing terminal and code action commands... + let getTerminal = () => { + if (!gooseTerminal || gooseTerminal.exitStatus !== undefined) { + vscode.window.showInformationMessage('goose agent starting, this may take a minute... ⏰'); + gooseTerminal = vscode.window.createTerminal({ + name: terminalName, + message: 'Loading Goose Session...' + }); + gooseTerminal.sendText('goose chat'); } + + console.log('Goose terminal created:', gooseTerminal.name); + gooseTerminal.show(); + + return gooseTerminal; + } + + let openGooseTerminal = vscode.commands.registerCommand('extension.openGoose', () => { + getTerminal(); }); - + context.subscriptions.push(openGooseTerminal); + let openTerminalDisposable = vscode.commands.registerCommand('extension.openGooseTerminal', () => { + getTerminal(); + }); + context.subscriptions.push(openTerminalDisposable); const askGooseToFix = vscode.commands.registerCommand('extension.askGooseToFix', async () => { const editor = vscode.window.activeTextEditor; @@ -165,4 +277,10 @@ let sendToGooseDisposable = vscode.commands.registerCommand('extension.sendToGoo } - +function createTempFileWithLines(selectedText: string, startLine: number): string { + const selectedLines = selectedText.split('\n').map((line, index) => `${startLine + index}: ${line}`).join('\n'); + const tempDir = os.tmpdir(); + const tempFileName = path.join(tempDir, `goose_context_${Date.now()}.txt`); + fs.writeFileSync(tempFileName, selectedLines); + return tempFileName; +}