diff --git a/package.json b/package.json index 9b4cfdb..7337ac2 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,11 @@ "icon": "$(copilot)", "title": "%commands.dachat.analyzeCsv.title%", "shortTitle": "%commands.dachat.analyzeCsv.shortTitle%" + }, + { + "category": "Data Analysis", + "command": "dachat.reportIssue", + "title": "Report Issue..." } ], "menus": { @@ -63,6 +68,10 @@ { "command": "dachat.analyzeCsv", "when": "false" + }, + { + "command": "dachat.reportIssue", + "when": "true" } ], "editor/title": [ diff --git a/src/extension.ts b/src/extension.ts index 3ac75a1..f5b41fa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { registerCsvCommand } from './csvCommand'; import { DataAgent } from './dataAgent'; +import { registerIssueReporter } from './issueReporter'; import { initializeLogger } from './logger'; import { FindFilesTool, InstallPythonPackageTool, RunPythonTool } from './tools'; @@ -14,6 +15,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(logger); context.subscriptions.push(dataAgent); context.subscriptions.push(registerCsvCommand()); + context.subscriptions.push(registerIssueReporter(context)); context.subscriptions.push(vscode.lm.registerTool(FindFilesTool.Id, new FindFilesTool(context))); const pythonTool = new RunPythonTool(context); context.subscriptions.push(vscode.lm.registerTool(RunPythonTool.Id, pythonTool)); diff --git a/src/issueReporter.ts b/src/issueReporter.ts new file mode 100644 index 0000000..3f99712 --- /dev/null +++ b/src/issueReporter.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation and GitHub. All rights reserved. +*--------------------------------------------------------------------------------------------*/ + +import { commands, ExtensionContext } from 'vscode'; +import { getLastErrors } from './logger'; + +export function registerIssueReporter(context: ExtensionContext) { + return commands.registerCommand('dachat.reportIssue', () => { + commands.executeCommand('workbench.action.openIssueReporter', { + extensionId: context.extension.id, + issueBody: issueBody, + data: getIssueData() + }); + }); +} + +const issueBody = ` + +# Behaviour + +XXX + +## Steps to reproduce: + +1. XXX + + + + + + + +# Outputs + +
+ +Output from Data Analysis Output Panel + +

+ +\`\`\` +XXX +\`\`\` + +

+
+`; + + +function getIssueData() { + const error = getLastErrors().trim(); + if (!error) { + return ''; + } + return ` +
+Last few Errors +

+ +\`\`\` +${error} +\`\`\` +

+
+`; +}; diff --git a/src/logger.ts b/src/logger.ts index e236021..7d5b12d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -3,9 +3,15 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionContext, ExtensionMode, LogOutputChannel, window } from "vscode"; +import { StopWatch } from "./platform/common/stopwatch"; let logger: LogOutputChannel; +const lastSeenError = { + timer: new StopWatch(), + error: '' +} + export function initializeLogger(extensionContext: ExtensionContext) { if (!logger) { logger = window.createOutputChannel('Data Analysis', { log: true }); @@ -17,9 +23,32 @@ export function initializeLogger(extensionContext: ExtensionContext) { debug.bind(logger)(message, ...args); }; + const error = logger.error; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logger.error = (errorMsg: string | Error, ...args: any[]) => { + // Get track of the last known error for issue reporting purposes. + lastSeenError.timer.reset(); + lastSeenError.error = [`${getTime()} ${errorMsg.toString()}`].concat(args.map(arg => `${arg}`)).join('\n'); + error.bind(logger)(errorMsg, ...args); + } } return logger; } -export { logger }; + +function getTime() { + const now = new Date(); + return now.toTimeString().split(' ')[0]; +} + +function getLastErrors() { + // If we haven't see any errors in the past 20 minutes, no point reporting any old errors. + if (!lastSeenError.error || lastSeenError.timer.elapsedTime > 20 * 60 * 1000) { + return ''; + } + return lastSeenError.error; +} + +export { getLastErrors, logger }; + diff --git a/src/platform/common/stopwatch.ts b/src/platform/common/stopwatch.ts new file mode 100644 index 0000000..9f2a91e --- /dev/null +++ b/src/platform/common/stopwatch.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation and GitHub. All rights reserved. +*--------------------------------------------------------------------------------------------*/ + + +/** + * Tracks wall clock time. Start time is set at contruction. + */ +export class StopWatch { + private started = Date.now(); + public get elapsedTime() { + return Date.now() - this.started; + } + public reset() { + this.started = Date.now(); + } +} diff --git a/src/tools.ts b/src/tools.ts index 3f74ec3..54fd3cb 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -78,7 +78,7 @@ export class RunPythonTool implements vscode.LanguageModelTool logger.error(`Pyodide => ${message}`, ...args), // eslint-disable-next-line @typescript-eslint/no-explicit-any - info: (message: string, ...args: any[]) => logger.info(`Pyodide => ${message}`, ...args) + info: (message: string, ...args: any[]) => logger.debug(`Pyodide => ${message}`, ...args) } }); } @@ -88,19 +88,13 @@ export class RunPythonTool implements vscode.LanguageModelTool `); - logger.debug(code); + logger.info(`Executing Python Code for "${options.input.reason || ''}"`); + logger.info(`Code => `, code); this.pendingRequests = this.pendingRequests.finally().then(() => this._kernel.execute(code)); const result = await this.pendingRequests as Awaited>; - logger.debug(`Result => `); - Object.keys(result || {}).forEach(key => { - logger.debug(`${key} :`); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - logger.debug((result as any)[key]); - }); + logger.debug(`Result => `, JSON.stringify(result)); const content: (vscode.LanguageModelPromptTsxPart | vscode.LanguageModelTextPart)[] = [] if (result && result['text/plain']) { @@ -112,7 +106,9 @@ export class RunPythonTool implements vscode.LanguageModelTool, token: vscode.CancellationToken ) { - logger.debug(`Installing Package "${options.input.package}"`); + logger.info(`Installing Package "${options.input.package}"`); const result = await this.pythonTool.invoke({ input: { code: `import ${options.input.package}`, @@ -197,8 +193,7 @@ export class InstallPythonPackageTool implements vscode.LanguageModelTool `); - logger.debug(JSON.stringify(result)); + logger.debug(`Result after installing package ${options.input.package} => `, JSON.stringify(result)); return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart('Installation successful')]); }