diff --git a/CHANGELOG.md b/CHANGELOG.md
index e61b5fea..be7c57f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,27 @@
+# [0.103.0](https://github.com/getappmap/vscode-appland/compare/v0.102.0...v0.103.0) (2023-10-31)
+
+
+### Features
+
+* Update @appland/components to 3.9.0 ([fa2cc6e](https://github.com/getappmap/vscode-appland/commit/fa2cc6e56f96b13225d1a865a3972a4495eecf28))
+
+# [0.102.0](https://github.com/getappmap/vscode-appland/compare/v0.101.2...v0.102.0) (2023-10-11)
+
+
+### Bug Fixes
+
+* AppMaps displayed chronologically for Java requests ([#813](https://github.com/getappmap/vscode-appland/issues/813)) ([4f8e730](https://github.com/getappmap/vscode-appland/commit/4f8e730be089000d8c2eb3e28b1226f39efaf0e4))
+* Instructions page should be reactive to the Java agent file ([3bfbcb6](https://github.com/getappmap/vscode-appland/commit/3bfbcb602e53e19285e902a56e427c7f07fe2459))
+
+
+### Features
+
+* Accurately determine if Runtime Analysis step is complete ([#829](https://github.com/getappmap/vscode-appland/issues/829)) ([0dd1fa2](https://github.com/getappmap/vscode-appland/commit/0dd1fa2d61e5b7923c67bf0d1e051b2074df3120))
+* The Code Objects view also lists External Service Calls ([d9b9c32](https://github.com/getappmap/vscode-appland/commit/d9b9c325edb40791d94353ccd3541baab1d241fa))
+* Update [@appland](https://github.com/appland) dependencies ([d696a53](https://github.com/getappmap/vscode-appland/commit/d696a5388d158f827c97caf5db4242340d8b44eb))
+* update @appland/components to 3.8.0 ([8b66898](https://github.com/getappmap/vscode-appland/commit/8b668983fe30602f5960836fe1d1120d6b28b31f))
+* **deleteAppMaps.ts:** add closeEditorByUri function after deleting appmaps ([2a54b8c](https://github.com/getappmap/vscode-appland/commit/2a54b8cde5c5b0b5894185cfe63f78c0cd539e4e))
+
## [0.101.2](https://github.com/getappmap/vscode-appland/compare/v0.101.1...v0.101.2) (2023-09-21)
diff --git a/extensions.json b/extensions.json
new file mode 100644
index 00000000..d54a2f2e
--- /dev/null
+++ b/extensions.json
@@ -0,0 +1 @@
+[{"identifier":{"id":"undefined_publisher.undefined"},"location":{"$mid":1,"fsPath":"/Users/kgilpin/source/appland/vscode-appland/extern","path":"/Users/kgilpin/source/appland/vscode-appland/extern","scheme":"file"},"relativeLocation":"extern"},{"identifier":{"id":"appland.appmap","uuid":"41d86b02-68d3-4049-9422-95da6d11cc2e"},"version":"0.90.1","location":{"$mid":1,"fsPath":"/Users/kgilpin/source/appland/vscode-appland/out","path":"/Users/kgilpin/source/appland/vscode-appland/out","scheme":"file"},"relativeLocation":"out","metadata":{"id":"41d86b02-68d3-4049-9422-95da6d11cc2e","publisherDisplayName":"AppLand","publisherId":"f7f1004e-6038-49cd-a096-4e618fe53f77","isPreReleaseVersion":false}}]
\ No newline at end of file
diff --git a/package.json b/package.json
index 2088242f..94026c71 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"name": "appmap",
"displayName": "AppMap",
"description": "Interactive maps of runtime code behavior",
- "version": "0.101.2",
+ "version": "0.103.0",
"repository": {
"type": "git",
"url": "https://github.com/getappmap/vscode-appland"
@@ -131,6 +131,22 @@
"command": "appmap.openCodeObjectInAppMap",
"title": "AppMap: Open Code Object in AppMap"
},
+ {
+ "command": "appmap.fixFinding",
+ "title": "AppMap: Suggest a Fix for an Analysis Finding"
+ },
+ {
+ "command": "appmap.fixTest",
+ "title": "AppMap: Suggest a Fix for a Failing Test"
+ },
+ {
+ "command": "appmap.ask",
+ "title": "AppMap: Ask a Question About the Current Code Selection"
+ },
+ {
+ "command": "appmap.generate",
+ "title": "AppMap: Generate Code Based on the Current Code Selection"
+ },
{
"command": "appmap.touchOutOfDateTestFiles",
"title": "AppMap: Touch Out-of-Date Test Files"
@@ -145,14 +161,24 @@
},
{
"command": "appmap.context.openAsJson",
- "title": "AppMap View: Open as JSON",
+ "title": "Open as JSON",
"icon": "$(bracket)"
},
{
"command": "appmap.context.deleteAppMap",
- "title": "AppMap View: Delete AppMap",
+ "title": "Delete",
"icon": "$(trash)"
},
+ {
+ "command": "appmap.context.fixTest",
+ "title": "Fix",
+ "icon": "$(wrench)"
+ },
+ {
+ "command": "appmap.context.fixFinding",
+ "title": "Fix",
+ "icon": "$(wrench)"
+ },
{
"command": "appmap.context.rename",
"title": "AppMap View: Rename AppMap"
@@ -376,6 +402,16 @@
"when": "view == appmap.views.appmaps && viewItem == appmap.views.appmaps.appMap",
"group": "inline"
},
+ {
+ "command": "appmap.context.fixTest",
+ "when": "view == appmap.views.findings && viewItem == appmap.views.analysis.failedTest",
+ "group": "inline"
+ },
+ {
+ "command": "appmap.context.fixFinding",
+ "when": "view == appmap.views.findings && viewItem == appmap.views.analysis.finding",
+ "group": "inline"
+ },
{
"command": "appmap.context.openInFileExplorer",
"when": "view == appmap.views.appmaps && viewItem == appmap.views.appmaps.appMap"
@@ -498,6 +534,7 @@
"diff": "^5.1.0",
"jquery": "^3.5.1",
"js-yaml": "^4.1.0",
+ "openai": "^3.3.0",
"popper.js": "^1.16.1",
"proper-lockfile": "^4.1.2",
"semver": "^7.3.5",
diff --git a/src/commands/ask.ts b/src/commands/ask.ts
new file mode 100644
index 00000000..9493e231
--- /dev/null
+++ b/src/commands/ask.ts
@@ -0,0 +1,220 @@
+import * as vscode from 'vscode';
+import LineInfoIndex, { LineInfo } from '../services/lineInfoIndex';
+import assert from 'assert';
+import AppMapCollection from '../services/appmapCollection';
+import { promptForAppMap } from '../lib/promptForAppMap';
+import { debug } from 'console';
+import { readFile, writeFile } from 'fs/promises';
+import { AppMap, AppMapFilter, CodeObject, buildAppMap } from '@appland/models';
+import buildOpenAIApi from '../lib/buildOpenAIApi';
+import { join } from 'path';
+import { FormatType, Specification, buildDiagram, format } from '@appland/sequence-diagram';
+import { randomUUID } from 'crypto';
+import { Completion, Question } from '../lib/ask';
+import selectedCode from '../lib/ask/selectedCode';
+import contextAppMap from '../lib/ask/contextAppMap';
+
+class LineInfoQuickPickItem implements vscode.QuickPickItem {
+ constructor(public lineInfo: LineInfo) {}
+
+ get label(): string {
+ return [
+ `line ${this.lineInfo.line}`,
+ this.lineInfo.codeObjects?.map((co) => co.fqid).join(', '),
+ ].join(': ');
+ }
+}
+
+export default function register(
+ context: vscode.ExtensionContext,
+ lineInfoIndex: LineInfoIndex,
+ appmapCollection: AppMapCollection
+): void {
+ context.subscriptions.push(
+ vscode.commands.registerCommand('appmap.ask', async () => {
+ const { activeTextEditor } = vscode.window;
+ if (!activeTextEditor) return;
+
+ const selection = selectedCode(activeTextEditor);
+ if (!selection) return;
+
+ const documentUri = activeTextEditor.document.uri;
+ const workspaceFolder = vscode.workspace.getWorkspaceFolder(documentUri);
+ if (!workspaceFolder) return;
+
+ const lineInfo = (await lineInfoIndex.lineInfo(documentUri)).filter(
+ (line) => line.codeObjects
+ );
+ if (lineInfo.length === 0) {
+ vscode.window.showErrorMessage(
+ `I couldn't find any AppMaps related to ${documentUri.path}`
+ );
+ return;
+ }
+
+ const lineCodePrompt =
+ selection.text.split('\n').length === 0
+ ? selection.text
+ : [selection.text.split('\n')[0], '...'].join('');
+ const question = await vscode.window.showInputBox({
+ placeHolder: `Ask a question about: ${lineCodePrompt}`,
+ value: 'How does this code work?',
+ });
+ if (!question) return;
+
+ const distanceFromSelection = (item: LineInfoQuickPickItem): number =>
+ Math.min(
+ Math.abs(item.lineInfo.line - selection.startLine),
+ Math.abs(item.lineInfo.line - selection.endLine)
+ );
+
+ const lineInfoItems = lineInfo.map((info) => new LineInfoQuickPickItem(info));
+ const lineDistance = lineInfoItems.map(distanceFromSelection);
+ const preferredDistance = lineDistance.sort()[0];
+
+ const choiceItems: vscode.QuickPickItem[] = [
+ ...lineInfoItems.filter(
+ (lineInfo) => distanceFromSelection(lineInfo) === preferredDistance
+ ),
+ { label: '---' },
+ ...lineInfoItems.filter(
+ (lineInfo) => distanceFromSelection(lineInfo) !== preferredDistance
+ ),
+ ];
+
+ const lineAbout = await vscode.window.showQuickPick(choiceItems, {
+ placeHolder: `Verify the function that you want to ask about`,
+ });
+ if (!lineAbout) return;
+ if (!(lineAbout instanceof LineInfoQuickPickItem)) return;
+
+ const codeObjectEntry = ((lineAbout as LineInfoQuickPickItem).lineInfo.codeObjects || [])[0];
+ assert(codeObjectEntry);
+
+ if (codeObjectEntry.appMapFiles.length === 0) {
+ console.warn(`No AppMaps for ${codeObjectEntry.fqid}`);
+ return;
+ }
+
+ let appMapFileName: string;
+ if (codeObjectEntry.appMapFiles.length === 1) {
+ appMapFileName = codeObjectEntry.appMapFiles[0];
+ } else {
+ const appmapFiles = new Set(Object.keys(await codeObjectEntry.appMapMetadata()));
+ const appmaps = appmapCollection
+ .allAppMaps()
+ .filter((appmap) => appmapFiles.has(appmap.descriptor.resourceUri.fsPath));
+
+ const selectedAppMap = await promptForAppMap(appmaps);
+ if (!selectedAppMap) return;
+
+ appMapFileName = selectedAppMap.fsPath;
+ }
+
+ const q = new Question(selection.text, question);
+
+ const appmapUri = vscode.Uri.file(appMapFileName);
+ {
+ const data = await readFile(appmapUri.fsPath, 'utf-8');
+ const appmap = buildAppMap().source(data).build();
+
+ let codeObject: CodeObject | undefined;
+ appmap.classMap.visit((co) => {
+ if (co.fqid === codeObjectEntry.fqid) codeObject = co;
+ });
+ if (!codeObject) {
+ vscode.window.showInformationMessage(
+ `Could not find code object ${codeObjectEntry.fqid} in the AppMap. Maybe it's out of date?`
+ );
+ return;
+ }
+ q.codeObject = codeObject;
+
+ const { events } = appmap;
+ let filterAppMap: AppMap;
+ {
+ const codeObjectEvents = events.filter(
+ (event) => event.codeObject.fqid === codeObjectEntry.fqid
+ );
+ q.returnValues = [
+ ...new Set(
+ codeObjectEvents
+ .map((e) => {
+ return e.returnValue
+ ? [e.returnValue.class, e.returnValue.value].join(': ')
+ : undefined;
+ })
+ .filter(Boolean)
+ ),
+ ] as string[];
+ filterAppMap = contextAppMap(appmap, codeObjectEvents);
+
+ q.scopeCodeObjects = [...new Set(appmap.rootEvents().map((e) => e.codeObject))];
+ }
+
+ {
+ const filterAppMapData = JSON.stringify(filterAppMap, null, 2);
+ let appmapName = ['ask_appmap', randomUUID()].join('-'); // question.replace(/[^a-zA-Z0-9]/g, '-'); KEG: Can result in a too-long file name.
+ if (appmapName.endsWith('-')) appmapName = appmapName.slice(0, -1);
+ const filePath = join(workspaceFolder?.uri.fsPath, `${appmapName}.appmap.json`);
+ await writeFile(filePath, filterAppMapData);
+ const uri = vscode.Uri.file(filePath);
+ await vscode.commands.executeCommand('vscode.openWith', uri, 'appmap.views.appMapFile');
+ }
+
+ const specification = Specification.build(filterAppMap, { loops: true });
+ assert(appmapUri);
+ let sequenceDiagramAppMap: AppMap;
+ {
+ const sequenceDiagramFilter = new AppMapFilter();
+ if (appmap.metadata.language?.name !== 'java')
+ sequenceDiagramFilter.declutter.hideExternalPaths.on = true;
+ sequenceDiagramAppMap = sequenceDiagramFilter.filter(filterAppMap, []);
+ }
+ const diagram = buildDiagram(appmapUri.fsPath, sequenceDiagramAppMap, specification);
+ q.sequenceDiagram = format(FormatType.PlantUML, diagram, appmapUri.fsPath).diagram;
+ }
+
+ const openAI = await buildOpenAIApi(context);
+ if (!openAI) return;
+
+ let completion: Completion | undefined;
+ await vscode.window.withProgress(
+ {
+ title: `Thinking about your question...`,
+ location: vscode.ProgressLocation.Notification,
+ },
+ async () => {
+ try {
+ completion = await q.complete(openAI);
+ } catch (e) {
+ debug(e);
+ vscode.window.showErrorMessage(`Unable to process your question: ${e}`);
+ }
+ }
+ );
+ if (!completion) return;
+
+ const fence = '```';
+ const responseText = [
+ `## ${lineCodePrompt}`,
+ `### You asked`,
+ `${fence}${question}${fence}`,
+ `### AI response:`,
+ completion.response,
+ `## Prompt
+
+Click to expand
+
+${fence}
+${completion.prompt}
+${fence}
+ `,
+ ].join('\n\n');
+ const newDocument = await vscode.workspace.openTextDocument({
+ content: responseText,
+ });
+ await vscode.window.showTextDocument(newDocument);
+ })
+ );
+}
diff --git a/src/commands/fixFinding.ts b/src/commands/fixFinding.ts
new file mode 100644
index 00000000..d1da3a49
--- /dev/null
+++ b/src/commands/fixFinding.ts
@@ -0,0 +1,163 @@
+import * as vscode from 'vscode';
+import { ResolvedFinding } from '../services/resolvedFinding';
+import AnalysisManager from '../services/analysisManager';
+import {
+ ChatCompletionRequestMessage,
+ ChatCompletionRequestMessageRoleEnum,
+ Configuration,
+ OpenAIApi,
+} from 'openai';
+import assert from 'assert';
+import { Event, buildAppMap } from '@appland/models';
+import { readFile } from 'fs/promises';
+import loadSnippet, { DEFAULT_SPAN } from '../lib/snippet';
+import { suggestFix } from '../lib/suggestFix';
+import { debug } from 'console';
+
+export class FindingPickItem implements vscode.QuickPickItem {
+ constructor(public finding: ResolvedFinding) {}
+
+ get label(): string {
+ return [this.finding.finding.ruleTitle, `(${this.finding.finding.hash_v2.slice(0, 8)})`].join(
+ ' '
+ );
+ }
+}
+
+export async function fixFinding(finding: ResolvedFinding, openAI: OpenAIApi) {
+ const appmap = buildAppMap()
+ .source(await readFile(finding.finding.appMapFile, 'utf-8'))
+ .build();
+ const language = appmap.metadata?.language?.name;
+
+ const scopeEvent = appmap.events[finding.finding.scope.id - 1];
+ const event = appmap.events[finding.finding.event.id - 1];
+ const stackFunctions: string[] = [];
+ {
+ let parent: Event | undefined = event;
+ while (parent) {
+ stackFunctions.push(parent.codeObject.fqid);
+ parent = parent.parent;
+ }
+ }
+
+ let participatingEvents: Event[] = Object.values(finding.finding.participatingEvents || {});
+ if (participatingEvents.length === 0) participatingEvents = [finding.finding.event];
+
+ const locations = [
+ ...participatingEvents.map((event) => [event.path, event.lineno].filter(Boolean).join(':')),
+ ...finding.finding.stack,
+ ];
+
+ const snippetLocations = new Set();
+ const codeSnippets = new Array();
+ for (const location of locations) {
+ if (snippetLocations.has(location)) continue;
+
+ // TODO: Determine the function span more accurately
+ const snippet = await loadSnippet(finding.folder, location, 0, DEFAULT_SPAN * 2);
+ if (!snippet) continue;
+
+ if (snippet) snippetLocations.add(location);
+ codeSnippets.push(['', `Source file: ${snippet.path}`, snippet.lines.join('\n')].join('\n'));
+ }
+
+ const systemMessages: ChatCompletionRequestMessage[] = [
+ {
+ content: `You are a software developer fixing problems in a codebase`,
+ role: 'system',
+ },
+ ];
+ if (language)
+ systemMessages.push({ content: `Programming language: ${language}`, role: 'system' });
+
+ let relatedReferences: string[] = [];
+ if (finding.rule.references && Object.keys(finding.rule.references).length > 0)
+ relatedReferences = Object.entries(finding.rule.references || {})
+ .map(([key, value]) => [key, value].join(' '))
+ .map((reference) => `Related reference: ${reference}`);
+
+ const userMessages: ChatCompletionRequestMessage[] = [
+ `The code contains a ${finding.rule.impactDomain} problem: ${finding.rule.title}`,
+ `Specifically: ${finding.finding.message}`,
+ ...relatedReferences,
+ `The problem occurs within ${scopeEvent.codeObject.fqid}. It's related to the following code:`,
+ ...codeSnippets,
+ ].map((message) => ({
+ content: message,
+ role: 'user' as ChatCompletionRequestMessageRoleEnum,
+ }));
+ userMessages.push({
+ content: `Describe the problem in the style of a code review comment, then suggest a fixed version of the code by generating one or more complete ${
+ language ? language : ''
+ } functions`,
+ role: 'user' as ChatCompletionRequestMessageRoleEnum,
+ });
+
+ await suggestFix(openAI, finding.rule.title, systemMessages, userMessages);
+}
+
+export default function register(context: vscode.ExtensionContext): void {
+ context.subscriptions.push(
+ vscode.commands.registerCommand('appmap.fixFinding', async (findingHash?: string) => {
+ const findingsIndex = AnalysisManager.findingsIndex;
+ if (!findingsIndex) return;
+
+ if (!findingHash) {
+ const uniqueFindingHashes = new Set();
+ const findings = (
+ findingsIndex
+ .findings()
+ .map((finding) => {
+ if (uniqueFindingHashes.has(finding.finding.hash_v2)) return;
+
+ uniqueFindingHashes.add(finding.finding.hash_v2);
+ return finding;
+ })
+
+ .filter(Boolean) as ResolvedFinding[]
+ ).sort((a, b) => a.rule.title.localeCompare(b.rule.title));
+
+ const choices = findings.map((finding) => new FindingPickItem(finding));
+
+ const selection = await vscode.window.showQuickPick(choices);
+ if (!selection) return;
+
+ findingHash = selection.finding.finding.hash_v2;
+ }
+ if (!findingHash) return;
+
+ const findings = findingsIndex.findingsByHash(findingHash);
+ if (!findings || findings.length === 0) return;
+
+ const finding = findings[0];
+ assert(finding);
+
+ let gptKey = await context.secrets.get('openai.gptKey');
+ if (!gptKey) {
+ gptKey = await vscode.window.showInputBox({ title: `Enter your OpenAI API key` });
+ if (!gptKey) return;
+
+ await context.secrets.store('openai.gptKey', gptKey);
+ }
+
+ const openAI = new OpenAIApi(new Configuration({ apiKey: gptKey }));
+
+ await vscode.window.withProgress(
+ {
+ title: `Analyzing: ${finding.rule.title}`,
+ location: vscode.ProgressLocation.Notification,
+ },
+ async () => {
+ assert(findingHash);
+ try {
+ await fixFinding(finding, openAI);
+ } catch (e) {
+ debug(e);
+ vscode.window.showErrorMessage(`Failed to analyze finding: ${e}`);
+ }
+ }
+ );
+ })
+ );
+}
diff --git a/src/commands/fixTest.ts b/src/commands/fixTest.ts
new file mode 100644
index 00000000..08d94a3b
--- /dev/null
+++ b/src/commands/fixTest.ts
@@ -0,0 +1,197 @@
+import * as vscode from 'vscode';
+import AppMapCollection from '../services/appmapCollection';
+import AppMapLoader from '../services/appmapLoader';
+import buildOpenAIApi from '../lib/buildOpenAIApi';
+import assert from 'assert';
+import {
+ ChatCompletionRequestMessage,
+ ChatCompletionRequestMessageRoleEnum,
+ OpenAIApi,
+} from 'openai';
+import loadSnippet, { DEFAULT_SPAN } from '../lib/snippet';
+import filesUnknownToGit from '../lib/filesUnknownToGit';
+import filesModifiedInGit from '../lib/filesModifiedInGit';
+import { suggestFix } from '../lib/suggestFix';
+import { debug } from 'console';
+import { isAbsolute } from 'path';
+
+export class AppMapPickItem implements vscode.QuickPickItem {
+ constructor(public appmap: AppMapLoader) {}
+
+ get label(): string {
+ return this.appmap.descriptor.metadata?.name || '';
+ }
+}
+
+async function fixFailedTest(appmapLoader: AppMapLoader, openAI: OpenAIApi) {
+ const appmap = await appmapLoader.loadAppMap();
+ const language = appmap.metadata?.language?.name;
+
+ const folder = vscode.workspace.workspaceFolders?.find((folder) =>
+ appmapLoader.descriptor.resourceUri.fsPath.startsWith(folder.uri.fsPath)
+ );
+ if (!folder) return;
+
+ const systemMessages: ChatCompletionRequestMessage[] = [
+ {
+ content: `You are a software developer fixing problems in a codebase`,
+ role: 'system',
+ },
+ ];
+ if (language)
+ systemMessages.push({ content: `Programming language: ${language}`, role: 'system' });
+
+ if (appmap.metadata.source_location) {
+ const snippet = await loadSnippet(folder, appmap.metadata.source_location, 0, DEFAULT_SPAN * 2);
+ if (snippet) {
+ const content = [
+ `Test case${snippet.lineno ? ' begins at line ' + snippet.lineno : ''}: ${snippet.path}`,
+ '',
+ snippet.lines.join('\n'),
+ ].join('\n');
+ systemMessages.push({ content, role: 'system' });
+ }
+ }
+
+ const userMessages: ChatCompletionRequestMessage[] = [];
+ if (appmap.metadata.test_failure?.message) {
+ userMessages.push({
+ content: `Test failed: ${appmap.metadata.test_failure.message}`,
+ role: 'user',
+ });
+ }
+
+ if (appmap.metadata.test_failure?.location) {
+ const snippet = await loadSnippet(folder, appmap.metadata.test_failure.location);
+ if (snippet) {
+ const content = [
+ '',
+ `Test failure${snippet.lineno ? ' occurred at line ' + snippet.lineno : ''}: ${
+ snippet.path
+ }`,
+ snippet.lines.join('\n'),
+ ].join('\n');
+ userMessages.push({ content, role: 'user' });
+ }
+ }
+
+ const uniqueExceptions = new Set();
+ for (const exceptionEvent of appmap.events.filter(
+ (event) => event.exceptions && event.exceptions.length
+ )) {
+ for (const exception of exceptionEvent.exceptions) {
+ const location = [exception.path, exception.lineno].filter(Boolean).join(':');
+ const exceptionId = [exception.class, location].filter(Boolean).join(':');
+ if (uniqueExceptions.has(exceptionId)) continue;
+
+ uniqueExceptions.add(exceptionId);
+
+ const snippet = await loadSnippet(folder, location);
+ if (!snippet) continue;
+
+ const content = [
+ '',
+ `Exception ${snippet.lineno ? ' occurred at line ' + snippet.lineno : ''}: ${snippet.path}`,
+ snippet.lines.join('\n'),
+ ].join('\n');
+ userMessages.push({ content, role: 'user' });
+ }
+ }
+
+ const outOfDateFiles = new Set([
+ ...(await filesUnknownToGit(folder.uri.fsPath)),
+ ...(await filesModifiedInGit(folder.uri.fsPath)),
+ ]);
+ const snippetLocations = new Set();
+ const snippetMessages: string[] = [];
+ for (const event of appmap.events) {
+ if (!(event.path && event.lineno)) continue;
+
+ let { path } = event;
+ if (isAbsolute(path) && path.startsWith(folder.uri.fsPath))
+ path = path.slice(folder.uri.fsPath.length + 1);
+
+ if (!outOfDateFiles.has(event.path)) continue;
+
+ const location = [path, event.lineno].join(':');
+ if (snippetLocations.has(location)) continue;
+ snippetLocations.add(location);
+
+ const snippet = await loadSnippet(folder, location, 0, DEFAULT_SPAN);
+ if (!snippet) continue;
+
+ const content = [
+ '',
+ `Code executed ${snippet.lineno ? ' at line ' + snippet.lineno : ''}: ${snippet.path}`,
+ snippet.lines.join('\n'),
+ ].join('\n');
+ snippetMessages.push(content);
+ }
+
+ userMessages.push(
+ ...snippetMessages
+ .slice(0, 4) // TODO: Limiting to 4 of these for now, to stay under the token limit
+ .map((content) => ({ content, role: 'user' as ChatCompletionRequestMessageRoleEnum }))
+ );
+
+ userMessages.push({
+ content: `Analyze the problem, then provide a fixed version of the code, by generating one or more complete ${
+ language ? language : ''
+ } functions`,
+ role: 'user' as ChatCompletionRequestMessageRoleEnum,
+ });
+
+ const title = appmap.metadata.test_failure?.message || appmap.metadata.name || 'Test failure';
+ await suggestFix(openAI, title, systemMessages, userMessages);
+}
+
+export default function register(
+ context: vscode.ExtensionContext,
+ appmapCollection: AppMapCollection
+): void {
+ context.subscriptions.push(
+ vscode.commands.registerCommand('appmap.fixTest', async (appmapUri?: vscode.Uri) => {
+ let appmap: AppMapLoader | undefined;
+ if (appmapUri) {
+ appmap = appmapCollection
+ .allAppMaps()
+ .find((appmap) => appmap.descriptor.resourceUri === appmapUri);
+ } else {
+ const choices = (
+ appmapCollection
+ .allAppMaps()
+ .filter(
+ (appmap) => appmap.descriptor.metadata?.test_status === 'failed'
+ ) as AppMapLoader[]
+ )
+ .map((appmap) => new AppMapPickItem(appmap))
+ .sort((a, b) => a.label.localeCompare(b.label));
+
+ const selection = await vscode.window.showQuickPick(choices);
+ if (!selection) return;
+
+ appmap = selection.appmap;
+ }
+ if (!appmap) return;
+
+ const openAI = await buildOpenAIApi(context);
+ if (!openAI) return;
+
+ await vscode.window.withProgress(
+ {
+ title: `Analyzing failed test`,
+ location: vscode.ProgressLocation.Notification,
+ },
+ async () => {
+ assert(appmap);
+ try {
+ await fixFailedTest(appmap, openAI);
+ } catch (e) {
+ debug(e);
+ vscode.window.showErrorMessage(`Failed to analyze failed test: ${e}`);
+ }
+ }
+ );
+ })
+ );
+}
diff --git a/src/commands/generate.ts b/src/commands/generate.ts
new file mode 100644
index 00000000..6645635a
--- /dev/null
+++ b/src/commands/generate.ts
@@ -0,0 +1,129 @@
+import * as vscode from 'vscode';
+import AppMapCollection from '../services/appmapCollection';
+import selectedCode from '../lib/ask/selectedCode';
+import { promptForAppMap } from '../lib/promptForAppMap';
+import { CodeGen, Completion } from '../lib/ask';
+import assert from 'assert';
+import buildOpenAIApi from '../lib/buildOpenAIApi';
+import { FormatType, Specification, buildDiagram, format } from '@appland/sequence-diagram';
+import { AppMap, AppMapFilter, buildAppMap } from '@appland/models';
+import contextAppMap from '../lib/ask/contextAppMap';
+import { readFile, writeFile } from 'fs/promises';
+import { randomUUID } from 'crypto';
+import { join } from 'path';
+import { debug } from 'console';
+
+export default function register(
+ context: vscode.ExtensionContext,
+ appmapCollection: AppMapCollection
+): void {
+ context.subscriptions.push(
+ vscode.commands.registerCommand('appmap.generate', async () => {
+ const documentUri = vscode.window.activeTextEditor?.document.uri;
+ if (!documentUri) return;
+
+ const workspaceFolder = vscode.workspace.getWorkspaceFolder(documentUri);
+ if (!workspaceFolder) return;
+
+ const selection = selectedCode(vscode.window.activeTextEditor);
+ if (!selection) return;
+
+ const lineCodePrompt =
+ selection.text.split('\n').length === 0
+ ? selection.text
+ : [selection.text.split('\n')[0], '...'].join('');
+ const question = await vscode.window.showInputBox({
+ placeHolder: `Generated code related to: ${lineCodePrompt}`,
+ });
+ if (!question) return;
+
+ const appmaps = appmapCollection.allAppMaps();
+ const appmapUri = await promptForAppMap(appmaps);
+ if (!appmapUri) return;
+
+ const g = new CodeGen(selection.text, question);
+
+ {
+ const appmapEntry = appmaps.find((appmap) => appmap.descriptor.resourceUri === appmapUri);
+ assert(appmapEntry);
+
+ if (appmapEntry.descriptor.metadata?.language?.name)
+ g.language = appmapEntry.descriptor.metadata.language.name;
+ }
+
+ let filterAppMap: AppMap;
+ {
+ const data = await readFile(appmapUri.fsPath, 'utf-8');
+ const appmap = buildAppMap().source(data).build();
+
+ filterAppMap = contextAppMap(appmap, appmap.rootEvents());
+
+ g.scopeCodeObjects = [...new Set(appmap.rootEvents().map((e) => e.codeObject))];
+ }
+
+ {
+ const filterAppMapData = JSON.stringify(filterAppMap, null, 2);
+ let appmapName = ['ask_appmap', randomUUID()].join('-'); // question.replace(/[^a-zA-Z0-9]/g, '-'); KEG: Can result in a too-long file name.
+ if (appmapName.endsWith('-')) appmapName = appmapName.slice(0, -1);
+ const filePath = join(workspaceFolder.uri.fsPath, `${appmapName}.appmap.json`);
+ await writeFile(filePath, filterAppMapData);
+ const uri = vscode.Uri.file(filePath);
+ await vscode.commands.executeCommand('vscode.openWith', uri, 'appmap.views.appMapFile');
+ }
+
+ const specification = Specification.build(filterAppMap, { loops: true });
+ assert(appmapUri);
+ let sequenceDiagramAppMap: AppMap;
+ {
+ const sequenceDiagramFilter = new AppMapFilter();
+ if (filterAppMap.metadata.language?.name !== 'java')
+ sequenceDiagramFilter.declutter.hideExternalPaths.on = true;
+ sequenceDiagramAppMap = sequenceDiagramFilter.filter(filterAppMap, []);
+ }
+ const diagram = buildDiagram(appmapUri.fsPath, sequenceDiagramAppMap, specification);
+ g.sequenceDiagram = format(FormatType.PlantUML, diagram, appmapUri.fsPath).diagram;
+
+ const openAI = await buildOpenAIApi(context);
+ if (!openAI) return;
+
+ let completion: Completion | undefined;
+ await vscode.window.withProgress(
+ {
+ title: `Generating code...`,
+ location: vscode.ProgressLocation.Notification,
+ },
+ async () => {
+ try {
+ completion = await g.complete(openAI);
+ } catch (e) {
+ if (e instanceof Error) debug(e.toString());
+ else debug(`${e}`);
+ vscode.window.showErrorMessage(`Unable to process your question: ${e}`);
+ }
+ }
+ );
+ if (!completion) return;
+
+ const fence = '```';
+ const responseText = [
+ `## ${lineCodePrompt}`,
+ `### You asked`,
+ `${fence}${question}${fence}`,
+ `### AI response:`,
+ completion.response,
+ `## Prompt
+
+Click to expand
+
+${fence}
+${completion.prompt}
+${fence}
+ `,
+ ].join('\n\n');
+ const newDocument = await vscode.workspace.openTextDocument({
+ content: responseText,
+ });
+ await vscode.window.showTextDocument(newDocument);
+ })
+ );
+}
diff --git a/src/extension.ts b/src/extension.ts
index 24bda976..ff0ee764 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -58,6 +58,10 @@ import getAppmapDir from './commands/getAppmapDir';
import JavaAssets from './services/javaAssets';
import checkAndTriggerFirstAppMapNotification from './lib/firstAppMapNotification';
import Watcher from './services/watcher';
+import { default as fixFinding } from './commands/fixFinding';
+import { default as fixTest } from './commands/fixTest';
+import { default as ask } from './commands/ask';
+import { default as generate } from './commands/generate';
export async function activate(context: vscode.ExtensionContext): Promise {
Telemetry.register(context);
@@ -192,6 +196,11 @@ export async function activate(context: vscode.ExtensionContext): Promise {
+ const propertyBasedUserMessages: string[] = [`A code snippet is: ${this.selectedCode}`];
+ if (this.codeObject)
+ propertyBasedUserMessages.push(
+ `The code snippet belongs to a function called: ${this.codeObject.fqid}`
+ );
+ if (this.returnValues && this.returnValues.length > 0)
+ propertyBasedUserMessages.push(`The code snippet returns ${this.returnValues.join(' or ')}`);
+ if (this.scopeCodeObjects && this.scopeCodeObjects.length > 0)
+ propertyBasedUserMessages.push(
+ `The code snippet is used by higher-level functions such as ${this.scopeCodeObjects
+ .map((co) => co.fqid)
+ .join(', ')}`
+ );
+ if (this.sequenceDiagram)
+ propertyBasedUserMessages.push(
+ `A PlantUML sequence diagram about this code flow is: ${this.sequenceDiagram}`
+ );
+
+ const systemMessages: ChatCompletionRequestMessage[] = this.systemMessages.map((message) => ({
+ content: message,
+ role: 'system' as ChatCompletionRequestMessageRoleEnum,
+ }));
+
+ const userMessages: ChatCompletionRequestMessage[] = [
+ ...propertyBasedUserMessages,
+ ...this.userMessages,
+ ].map((message) => ({
+ content: message,
+ role: 'user' as ChatCompletionRequestMessageRoleEnum,
+ }));
+
+ const messages = [...systemMessages, ...userMessages];
+ try {
+ const result = await ai.createChatCompletion({
+ model: this.model,
+ messages,
+ n: 1,
+ max_tokens:
+ // 16384 is the maximum number of tokens allowed by gpt-3.5-turbo-16k
+ 16384 -
+ // This is a conservative estimate of the number of tokens in the prompt, since
+ // a token can be more than one character.
+ (messages.map((msg) => msg.content?.length).filter(Boolean) as number[]).reduce(
+ (a, b) => a + b,
+ 0
+ ),
+ });
+ const response = result.data.choices
+ .map((choice) => (assert(choice.message), choice.message.content))
+ .filter(Boolean)
+ .join('\n');
+ return {
+ prompt: messages.map((msg) => [msg.role, msg.content].join(': ')).join('\n'),
+ response,
+ };
+ } catch (e) {
+ warn(e);
+ return;
+ }
+ }
+
+ protected abstract get systemMessages(): string[];
+
+ protected abstract get userMessages(): string[];
+}
+
+export class Question extends Ask {
+ public constructor(public readonly selectedCode: string, public readonly question: string) {
+ super(selectedCode, question);
+ }
+
+ protected get userMessages(): string[] {
+ return [`Answer the following question about the code: "${this.question}"`];
+ }
+
+ protected get systemMessages(): string[] {
+ return [
+ `You are a software developer explaining how code works.`,
+ `Be as assertive as possible. Make your best guess based on the information you have available.`,
+ `Try to include URL routes, SQL queries, function names, and code snippets.`,
+ `Use a list format for the answer.`,
+ `Do not discuss software development in general terms.`,
+ `Do not make statements like "However, without seeing the complete codebase...".`,
+ ];
+ }
+}
+
+export class CodeGen extends Ask {
+ public constructor(public readonly selectedCode: string, public readonly question: string) {
+ super(selectedCode, question);
+ }
+
+ protected get userMessages(): string[] {
+ return [`Generate code to: "${this.question}"`];
+ }
+
+ protected get systemMessages(): string[] {
+ const messages = [
+ `You are a software developer writing code.`,
+ `Be as assertive as possible. Make your best guess based on the information you have available.`,
+ `Respond with code that is syntactically correct.`,
+ ];
+ if (this.language) messages.push(`Respond with code in ${this.language}.`);
+
+ return messages;
+ }
+}
diff --git a/src/lib/ask/contextAppMap.ts b/src/lib/ask/contextAppMap.ts
new file mode 100644
index 00000000..79b6a07e
--- /dev/null
+++ b/src/lib/ask/contextAppMap.ts
@@ -0,0 +1,47 @@
+import { AppMap, Event, EventNavigator, buildAppMap } from '@appland/models';
+
+export default function contextAppMap(appmap: AppMap, scopeEvents: Event[]): AppMap {
+ const { events } = appmap;
+
+ const roots: Event[] = [];
+ const filterEvents: Event[] = [...scopeEvents];
+ for (const event of scopeEvents) {
+ for (const ancestor of new EventNavigator(event).ancestors()) {
+ filterEvents.push(ancestor.event);
+ if (ancestor.event.httpServerRequest) {
+ roots.push(ancestor.event);
+ break;
+ }
+ if (!ancestor.event.parent) roots.push(ancestor.event);
+ }
+ for (const child of event.children) {
+ filterEvents.push(child);
+ for (const child2 of child.children) {
+ filterEvents.push(child2);
+ for (const child3 of child2.children) {
+ filterEvents.push(child3);
+ for (const child4 of child3.children) filterEvents.push(child4);
+ }
+ }
+ }
+ }
+ for (const root of roots) {
+ for (const child of root.children) {
+ filterEvents.push(child);
+ for (const child2 of child.children) {
+ filterEvents.push(child2);
+ for (const child3 of child2.children) {
+ filterEvents.push(child3);
+ for (const child4 of child3.children) filterEvents.push(child4);
+ }
+ }
+ }
+ }
+
+ const eventIds = new Set(filterEvents.map((e) => e.id));
+ return buildAppMap({
+ events: events.filter((e) => eventIds.has(e.callEvent.id)),
+ classMap: appmap.classMap.roots.map((c) => ({ ...c.data })),
+ metadata: appmap.metadata,
+ }).build();
+}
diff --git a/src/lib/ask/selectedCode.ts b/src/lib/ask/selectedCode.ts
new file mode 100644
index 00000000..d874029c
--- /dev/null
+++ b/src/lib/ask/selectedCode.ts
@@ -0,0 +1,20 @@
+import * as vscode from 'vscode';
+
+export type Selection = {
+ startLine: number;
+ endLine: number;
+ text: string;
+};
+
+export default function selectedCode(textEditor?: vscode.TextEditor): Selection | undefined {
+ if (!textEditor) return;
+
+ const { selection } = textEditor;
+ if (!selection) return;
+
+ const startLine = selection.anchor.line;
+ const endLine = selection.active.line;
+ let selectedCode = textEditor.document.getText(selection).trim();
+ if (selectedCode === '') selectedCode = textEditor.document.lineAt(startLine).text.trim();
+ return { startLine, endLine, text: selectedCode };
+}
diff --git a/src/lib/buildOpenAIApi.ts b/src/lib/buildOpenAIApi.ts
new file mode 100644
index 00000000..5ce2ceaa
--- /dev/null
+++ b/src/lib/buildOpenAIApi.ts
@@ -0,0 +1,17 @@
+import * as vscode from 'vscode';
+
+import { Configuration, OpenAIApi } from 'openai';
+
+export default async function buildOpenAIApi(
+ context: vscode.ExtensionContext
+): Promise {
+ let gptKey = await context.secrets.get('openai.gptKey');
+ if (!gptKey) {
+ gptKey = await vscode.window.showInputBox({ title: `Enter your OpenAI API key` });
+ if (!gptKey) return;
+
+ await context.secrets.store('openai.gptKey', gptKey);
+ }
+
+ return new OpenAIApi(new Configuration({ apiKey: gptKey }));
+}
diff --git a/src/lib/filesModifiedInGit.ts b/src/lib/filesModifiedInGit.ts
new file mode 100644
index 00000000..d49283a4
--- /dev/null
+++ b/src/lib/filesModifiedInGit.ts
@@ -0,0 +1,11 @@
+import { exec } from 'child_process';
+
+export default async function filesModifiedInGit(cwd: string): Promise {
+ return new Promise((resolve, reject) => {
+ exec(`git ls-files -m`, { cwd }, (err, stdout) => {
+ if (err && err.code && err.code > 0) reject(err.code);
+
+ resolve(stdout.trim().split('\n'));
+ });
+ });
+}
diff --git a/src/lib/filesUnknownToGit.ts b/src/lib/filesUnknownToGit.ts
new file mode 100644
index 00000000..20543b7e
--- /dev/null
+++ b/src/lib/filesUnknownToGit.ts
@@ -0,0 +1,11 @@
+import { exec } from 'child_process';
+
+export default async function filesUnknownToGit(cwd: string): Promise {
+ return new Promise((resolve, reject) => {
+ exec(`git ls-files -o --exclude-standard`, { cwd }, (err, stdout) => {
+ if (err && err.code && err.code > 0) reject(err.code);
+
+ resolve(stdout.trim().split('\n'));
+ });
+ });
+}
diff --git a/src/lib/snippet.ts b/src/lib/snippet.ts
new file mode 100644
index 00000000..b9da05a1
--- /dev/null
+++ b/src/lib/snippet.ts
@@ -0,0 +1,60 @@
+import * as vscode from 'vscode';
+
+import { isAbsolute } from 'path';
+
+export const DEFAULT_SPAN = 10;
+
+export type Snippet = {
+ path: string;
+ lineno?: number;
+ lines: string[];
+};
+
+export default async function loadSnippet(
+ folder: vscode.WorkspaceFolder,
+ location: string,
+ behindSpan = DEFAULT_SPAN,
+ aheadSpan = DEFAULT_SPAN
+): Promise {
+ const [path, lineStr] = location.split(':');
+ let lineno: number | undefined;
+ if (lineStr) lineno = parseInt(lineStr, 10);
+
+ let filePath = path;
+ if (isAbsolute(filePath) && filePath.startsWith(folder.uri.fsPath))
+ filePath = filePath.slice(folder.uri.fsPath.length + 1);
+
+ if (isAbsolute(filePath) || filePath.startsWith('node_modules') || filePath.startsWith('vendor'))
+ return;
+
+ const fileUri = vscode.Uri.joinPath(folder.uri, filePath);
+ let fileStat: vscode.FileStat;
+ let exists = false;
+ try {
+ fileStat = await vscode.workspace.fs.stat(fileUri);
+ exists = fileStat.type === vscode.FileType.File;
+ } catch (e) {
+ // File doesn't exist
+ }
+
+ if (!exists) return;
+
+ const fileContents = await vscode.workspace.fs.readFile(fileUri);
+ const lines = fileContents.toString().split('\n');
+ let snippet: string[];
+ if (lineno) {
+ const startLine = Math.max(lineno - behindSpan - 1, 0);
+ const endLine = Math.min(lineno + aheadSpan - 1, lines.length);
+ snippet = lines
+ .slice(startLine, endLine)
+ .map((line, index) => `${index + startLine + 1}: ${line}`);
+ } else {
+ snippet = lines;
+ }
+
+ return {
+ path: filePath,
+ lineno,
+ lines: snippet,
+ };
+}
diff --git a/src/lib/suggestFix.ts b/src/lib/suggestFix.ts
new file mode 100644
index 00000000..1d5273b4
--- /dev/null
+++ b/src/lib/suggestFix.ts
@@ -0,0 +1,58 @@
+import * as vscode from 'vscode';
+import assert from 'assert';
+import { ChatCompletionRequestMessage, CreateChatCompletionResponse, OpenAIApi } from 'openai';
+
+const MAX_TITLE = 30;
+
+export async function suggestFix(
+ openAI: OpenAIApi,
+ title: string,
+ systemMessages: ChatCompletionRequestMessage[],
+ userMessages: ChatCompletionRequestMessage[]
+) {
+ const messages = [...systemMessages, ...userMessages];
+
+ let response: CreateChatCompletionResponse;
+ try {
+ const result = await openAI.createChatCompletion({
+ model: 'gpt-3.5-turbo',
+ messages,
+ n: 1,
+ max_tokens:
+ // 4096 is the maximum number of tokens allowed by gpt-3.5-turbo
+ 4096 -
+ // This is a conservative estimate of the number of tokens in the prompt, since
+ // a token can be more than one character.
+ (messages.map((msg) => msg.content?.length).filter(Boolean) as number[]).reduce(
+ (a, b) => a + b,
+ 0
+ ),
+ });
+ response = result.data;
+ } catch (e) {
+ if (e instanceof Error) vscode.window.showErrorMessage(e.message);
+ else vscode.window.showErrorMessage(`Failed to analyze failed test: ${e}`);
+ return;
+ }
+
+ const promptContent = [
+ '```',
+ ...messages.map((msg) => [msg.role, msg.content].join(': ')),
+ '```',
+ ].join('\n');
+ const responseContent = response.choices
+ .filter((choice) => choice.message)
+ .map((choice) => (assert(choice.message), choice.message.content))
+ .filter(Boolean)
+ .join('\n');
+
+ title = title.replaceAll('\n', ' ');
+ if (title.length > MAX_TITLE) title = title.slice(0, MAX_TITLE) + '...';
+
+ const newDocument = await vscode.workspace.openTextDocument({
+ content: [`## Analysis of '${title}'`, responseContent, `## Prompt`, promptContent].join(
+ '\n\n'
+ ),
+ });
+ vscode.window.showTextDocument(newDocument);
+}
diff --git a/src/services/findingLocation.ts b/src/services/findingLocation.ts
index 4a3d48b5..520afcff 100644
--- a/src/services/findingLocation.ts
+++ b/src/services/findingLocation.ts
@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
-import { Finding } from '@appland/scanner/built/cli';
+import { Finding } from '@appland/scanner';
export type FindingLocation = {
finding: Finding;
diff --git a/src/services/findingsIndex.ts b/src/services/findingsIndex.ts
index ff8ffb2f..5578cb8b 100644
--- a/src/services/findingsIndex.ts
+++ b/src/services/findingsIndex.ts
@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
-import { Finding } from '@appland/scanner/built/cli';
+import { Finding } from '@appland/scanner';
import { ResolvedFinding } from './resolvedFinding';
import { debuglog, promisify } from 'util';
import { readFile } from 'fs';
diff --git a/src/services/lineInfoIndex.ts b/src/services/lineInfoIndex.ts
index 6fc78105..ca2fdac7 100644
--- a/src/services/lineInfoIndex.ts
+++ b/src/services/lineInfoIndex.ts
@@ -3,7 +3,7 @@ import * as vscode from 'vscode';
import ClassMapIndex from './classMapIndex';
import { CodeObjectEntry } from '../lib/CodeObjectEntry';
-class LineInfo {
+export class LineInfo {
public codeObjects?: CodeObjectEntry[];
constructor(public line: number) {}
diff --git a/src/tree/findingsTreeDataProvider.ts b/src/tree/findingsTreeDataProvider.ts
index c5d52740..c6f1a6e3 100644
--- a/src/tree/findingsTreeDataProvider.ts
+++ b/src/tree/findingsTreeDataProvider.ts
@@ -153,6 +153,23 @@ export class FindingsTreeDataProvider
);
appmaps.onUpdated(() => this._onDidChangeTreeData.fire(undefined));
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ 'appmap.context.fixTest',
+ async (item: FailedTestTreeItem) => {
+ vscode.commands.executeCommand('appmap.fixTest', item.appmap.descriptor.resourceUri);
+ }
+ )
+ );
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ 'appmap.context.fixFinding',
+ async (item: FindingTreeItem) => {
+ vscode.commands.executeCommand('appmap.fixFinding', item.finding.finding.hash_v2);
+ }
+ )
+ );
}
public setFindingsIndex(findingsIndex?: FindingsIndex): void {
diff --git a/test/integration/lib/indexJanitor.test.ts b/test/integration/lib/indexJanitor.test.ts
index 9eadfadd..5eda19fc 100644
--- a/test/integration/lib/indexJanitor.test.ts
+++ b/test/integration/lib/indexJanitor.test.ts
@@ -20,6 +20,7 @@ describe('AppMapIndex', () => {
beforeEach(async () => (extension = await waitForExtension()));
afterEach(initializeWorkspace);
+ // TODO: Restore this test once the IndexJanitor has been fixed or this test has been fixed.
xit('cleans up index directories', async () => {
const appmapFiles = await findFiles(`tmp/appmap/**/*.appmap.json`);
const indexDirs = appmapFiles.map(({ fsPath }) => fsPath.replace(/\.appmap\.json$/, ''));
diff --git a/yarn.lock b/yarn.lock
index ce7b5bcf..25ecb464 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2829,6 +2829,7 @@ __metadata:
mocha-suppress-logs: ^0.3.1
mockery: ^2.1.0
node-libs-browser: ^2.2.1
+ openai: ^3.3.0
openapi-types: ^11.0.1
popper.js: ^1.16.1
prettier: ^2.8.4
@@ -3125,6 +3126,15 @@ __metadata:
languageName: node
linkType: hard
+"axios@npm:^0.26.0":
+ version: 0.26.1
+ resolution: "axios@npm:0.26.1"
+ dependencies:
+ follow-redirects: ^1.14.8
+ checksum: d9eb58ff4bc0b36a04783fc9ff760e9245c829a5a1052ee7ca6013410d427036b1d10d04e7380c02f3508c5eaf3485b1ae67bd2adbfec3683704745c8d7a6e1a
+ languageName: node
+ linkType: hard
+
"axios@npm:^0.27.2":
version: 0.27.2
resolution: "axios@npm:0.27.2"
@@ -6332,7 +6342,7 @@ __metadata:
languageName: node
linkType: hard
-"follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.14.9":
+"follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.14.8, follow-redirects@npm:^1.14.9":
version: 1.15.2
resolution: "follow-redirects@npm:1.15.2"
peerDependenciesMeta:
@@ -9853,6 +9863,16 @@ __metadata:
languageName: node
linkType: hard
+"openai@npm:^3.3.0":
+ version: 3.3.0
+ resolution: "openai@npm:3.3.0"
+ dependencies:
+ axios: ^0.26.0
+ form-data: ^4.0.0
+ checksum: 28ccff8c09b6f47828c9583bb3bafc38a8459c76ea10eb9e08ca880f65523c5a9cc6c5f3c7669dded6f4c93e7cf49dd5c4dbfd12732a0f958c923117740d677b
+ languageName: node
+ linkType: hard
+
"openapi-diff@npm:^0.23.5, openapi-diff@npm:^0.23.6":
version: 0.23.6
resolution: "openapi-diff@npm:0.23.6"