diff --git a/extensions/positron-assistant/package-lock.json b/extensions/positron-assistant/package-lock.json index b97343939cb6..f668eda4fe6f 100644 --- a/extensions/positron-assistant/package-lock.json +++ b/extensions/positron-assistant/package-lock.json @@ -12,7 +12,9 @@ "@anthropic-ai/sdk": "^0.57.0", "@github/copilot-language-server": "^1.367.0", "@vscode/prompt-tsx": "^0.4.0-alpha.5", - "vscode-languageclient": "^9.0.1" + "squirrelly": "^9.1.0", + "vscode-languageclient": "^9.0.1", + "yaml": "^2.8.1" }, "devDependencies": { "@ai-sdk/amazon-bedrock": "2.2.12", @@ -1117,7 +1119,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1153,11 +1157,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.0", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1165,16 +1171,33 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.9.0", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1196,15 +1219,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1212,10 +1242,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -1351,7 +1384,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2394,7 +2429,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2477,7 +2514,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.14.0", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -2578,6 +2617,8 @@ }, "node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2727,7 +2768,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2762,6 +2805,8 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -2988,30 +3033,33 @@ } }, "node_modules/eslint": { - "version": "9.15.0", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3046,7 +3094,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3061,7 +3111,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3072,13 +3124,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3100,6 +3154,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3144,6 +3200,8 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, @@ -3175,6 +3233,8 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, @@ -3377,6 +3437,8 @@ }, "node_modules/globals": { "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -3484,7 +3546,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3649,6 +3713,8 @@ }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, @@ -4097,6 +4163,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -4165,6 +4233,8 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -4238,6 +4308,8 @@ }, "node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -4369,6 +4441,18 @@ "node": ">=0.3.1" } }, + "node_modules/squirrelly": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/squirrelly/-/squirrelly-9.1.0.tgz", + "integrity": "sha512-kvjFqb7qzC4gX4lkqSaU8QPvUHhDLMiDpxpz7a66vjTH0JtjLJqAXbPrc7ST61EefuuuW05sne2rjGskunrF2A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/squirrellyjs/squirrelly?sponsor=1" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4595,6 +4679,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4656,9 +4742,9 @@ } }, "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4774,6 +4860,18 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/extensions/positron-assistant/package.json b/extensions/positron-assistant/package.json index 438202dbb68f..4aeac8340cf6 100644 --- a/extensions/positron-assistant/package.json +++ b/extensions/positron-assistant/package.json @@ -186,6 +186,12 @@ "title": "%commands.toggleInlineCompletions.title%", "category": "%commands.category%", "enablement": "config.positron.assistant.enable" + }, + { + "command": "positron-assistant.managePromptFiles", + "title": "%commands.managePromptFiles.title%", + "category": "%commands.category%", + "enablement": "config.positron.assistant.enable && isDevelopment" } ], "configuration": [ @@ -698,7 +704,9 @@ "@anthropic-ai/sdk": "^0.57.0", "@github/copilot-language-server": "^1.367.0", "@vscode/prompt-tsx": "^0.4.0-alpha.5", - "vscode-languageclient": "^9.0.1" + "squirrelly": "^9.1.0", + "vscode-languageclient": "^9.0.1", + "yaml": "^2.8.1" }, "extensionDependencies": [ "positron.positron-supervisor" diff --git a/extensions/positron-assistant/package.nls.json b/extensions/positron-assistant/package.nls.json index 96864786e7a2..37b8e10691db 100644 --- a/extensions/positron-assistant/package.nls.json +++ b/extensions/positron-assistant/package.nls.json @@ -8,6 +8,7 @@ "commands.generateCommitMessage.title": "Generate Commit Message", "commands.cancelGenerateCommitMessage.title": "Cancel Generate Commit Message", "commands.toggleInlineCompletions.title": "Toggle (Enable/Disable) Completions", + "commands.managePromptFiles.title": "Manage Prompt Files", "commands.copilot.signin.title": "Copilot Sign In", "commands.copilot.signout.title": "Copilot Sign Out", "commands.category": "Positron Assistant", diff --git a/extensions/positron-assistant/src/commands/doc.ts b/extensions/positron-assistant/src/commands/doc.ts index 0df064073580..aa1002811601 100644 --- a/extensions/positron-assistant/src/commands/doc.ts +++ b/extensions/positron-assistant/src/commands/doc.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as fs from 'fs'; -import { MD_DIR } from '../constants'; -import { PositronAssistantEditorParticipant, PositronAssistantChatContext } from '../participants.js'; +import { PositronAssistantChatContext } from '../participants.js'; +import { PromptRenderer } from '../promptRender.js'; export const DOC_COMMAND = 'doc'; @@ -21,16 +20,10 @@ export async function docHandler( _token: vscode.CancellationToken, handleDefault: () => Promise ) { - const { systemPrompt } = context; - response.progress(vscode.l10n.t('Generating documentation...')); - const prompt = await fs.promises.readFile(`${MD_DIR}/prompts/chat/doc.md`, 'utf8'); - context.systemPrompt = `${systemPrompt}\n\n${prompt}`; + const prompt = PromptRenderer.renderCommandPrompt(DOC_COMMAND, _request, context).content; + context.systemPrompt += `\n\n${prompt}`; return handleDefault(); } - -export function registerDocCommand() { - PositronAssistantEditorParticipant.registerCommand(DOC_COMMAND, docHandler); -} diff --git a/extensions/positron-assistant/src/commands/explain.ts b/extensions/positron-assistant/src/commands/explain.ts index 6d4c4e9f6bdc..274208d8e1ff 100644 --- a/extensions/positron-assistant/src/commands/explain.ts +++ b/extensions/positron-assistant/src/commands/explain.ts @@ -4,11 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as fs from 'fs'; - -import { MD_DIR } from '../constants'; -import { PositronAssistantChatParticipant, PositronAssistantEditorParticipant, PositronAssistantChatContext } from '../participants.js'; - +import { PositronAssistantChatContext } from '../participants.js'; +import { PromptRenderer } from '../promptRender.js'; export const EXPLAIN_COMMAND = 'explain'; @@ -22,15 +19,9 @@ export async function explainHandler( _token: vscode.CancellationToken, handleDefault: () => Promise ) { - const defaultPrompt = await fs.promises.readFile(`${MD_DIR}/prompts/chat/default.md`, 'utf8'); - const explainPrompt = await fs.promises.readFile(`${MD_DIR}/prompts/chat/explain.md`, 'utf8'); - const warningPrompt = await fs.promises.readFile(`${MD_DIR}/prompts/chat/warning.md`, 'utf8'); - context.systemPrompt = defaultPrompt + '\n\n' + explainPrompt + '\n\n' + warningPrompt; - return handleDefault(); -} + const prompt = PromptRenderer.renderCommandPrompt(EXPLAIN_COMMAND, _request, context).content; + context.systemPrompt += `\n\n${prompt}`; -export function registerExplainCommand() { - PositronAssistantChatParticipant.registerCommand(EXPLAIN_COMMAND, explainHandler); - PositronAssistantEditorParticipant.registerCommand(EXPLAIN_COMMAND, explainHandler); + return handleDefault(); } diff --git a/extensions/positron-assistant/src/commands/fix.ts b/extensions/positron-assistant/src/commands/fix.ts index a416ae271c79..670d2a3be94e 100644 --- a/extensions/positron-assistant/src/commands/fix.ts +++ b/extensions/positron-assistant/src/commands/fix.ts @@ -4,11 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as fs from 'fs'; - -import { MD_DIR } from '../constants'; -import { ParticipantID, PositronAssistantChatParticipant, PositronAssistantEditorParticipant, PositronAssistantChatContext } from '../participants.js'; -import { PositronAssistantToolName } from '../types.js'; +import { PositronAssistantChatContext } from '../participants.js'; +import { PromptRenderer } from '../promptRender.js'; export const FIX_COMMAND = 'fix'; @@ -22,27 +19,10 @@ export async function fixHandler( _token: vscode.CancellationToken, handleDefault: () => Promise ) { - const { systemPrompt, participantId } = context; - - if (participantId !== ParticipantID.Chat && participantId !== ParticipantID.Editor) { - return handleDefault(); - } - response.progress('Preparing edits...'); - if (participantId === ParticipantID.Chat) { - const prompt = await fs.promises.readFile(`${MD_DIR}/prompts/chat/fix.md`, 'utf8'); - context.systemPrompt = `${systemPrompt}\n\n${prompt}`; - context.toolAvailability.set(PositronAssistantToolName.ProjectTree, true); - } else { - const prompt = await fs.promises.readFile(`${MD_DIR}/prompts/chat/fixEditor.md`, 'utf8'); - context.systemPrompt = `${systemPrompt}\n\n${prompt}`; - } + const prompt = PromptRenderer.renderCommandPrompt(FIX_COMMAND, _request, context).content; + context.systemPrompt += `\n\n${prompt}`; return handleDefault(); } - -export function registerFixCommand() { - PositronAssistantChatParticipant.registerCommand(FIX_COMMAND, fixHandler); - PositronAssistantEditorParticipant.registerCommand(FIX_COMMAND, fixHandler); -} diff --git a/extensions/positron-assistant/src/commands/index.ts b/extensions/positron-assistant/src/commands/index.ts index 480927ac88ee..e3d5132226d6 100644 --- a/extensions/positron-assistant/src/commands/index.ts +++ b/extensions/positron-assistant/src/commands/index.ts @@ -4,11 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { PositronAssistantChatContext } from '../participants.js'; -import { registerFixCommand } from './fix.js'; -import { registerQuartoCommand } from './quarto.js'; -import { registerExplainCommand } from './explain.js'; -import { registerDocCommand } from './doc.js'; +import * as positron from 'positron'; +import { FIX_COMMAND, fixHandler } from './fix.js'; +import { EXPORT_QUARTO_COMMAND, quartoHandler } from './quarto.js'; +import { EXPLAIN_COMMAND, explainHandler } from './explain.js'; +import { DOC_COMMAND, docHandler } from './doc.js'; +import { log } from '../extension.js'; +import { + PositronAssistantAgentParticipant, + PositronAssistantChatContext, + PositronAssistantChatParticipant, + PositronAssistantEditorParticipant, + PositronAssistantEditParticipant, + PositronAssistantNotebookParticipant, + PositronAssistantTerminalParticipant +} from '../participants.js'; +import { PromptRenderer } from '../promptRender.js'; /** * A function that handles chat requests. @@ -29,9 +40,38 @@ export interface IChatRequestHandler { ): Promise; } +function registerAssistantCommand(command: string, handler: IChatRequestHandler) { + const metadata = PromptRenderer.getCommandMetadata(command); + const modes = metadata.mode ?? []; + for (const mode of modes) { + switch (mode) { + case positron.PositronChatMode.Ask: + PositronAssistantChatParticipant.registerCommand(command, handler); + break; + case positron.PositronChatMode.Edit: + PositronAssistantEditParticipant.registerCommand(command, handler); + break; + case positron.PositronChatMode.Agent: + PositronAssistantAgentParticipant.registerCommand(command, handler); + break; + case positron.PositronChatAgentLocation.Editor: + PositronAssistantEditorParticipant.registerCommand(command, handler); + break; + case positron.PositronChatAgentLocation.Terminal: + PositronAssistantTerminalParticipant.registerCommand(command, handler); + break; + case positron.PositronChatAgentLocation.Notebook: + PositronAssistantNotebookParticipant.registerCommand(command, handler); + break; + default: + log.trace('[commands] Unsupported command mode:', mode); + } + } +} + export function registerAssistantCommands() { - registerFixCommand(); - registerExplainCommand(); - registerQuartoCommand(); - registerDocCommand(); + registerAssistantCommand(DOC_COMMAND, docHandler); + registerAssistantCommand(FIX_COMMAND, fixHandler); + registerAssistantCommand(EXPLAIN_COMMAND, explainHandler); + registerAssistantCommand(EXPORT_QUARTO_COMMAND, quartoHandler); } diff --git a/extensions/positron-assistant/src/commands/quarto.ts b/extensions/positron-assistant/src/commands/quarto.ts index cdb05e566977..63e3f93dfc0d 100644 --- a/extensions/positron-assistant/src/commands/quarto.ts +++ b/extensions/positron-assistant/src/commands/quarto.ts @@ -4,11 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as fs from 'fs'; - -import { MD_DIR } from '../constants'; import { toLanguageModelChatMessage } from '../utils'; -import { PositronAssistantChatParticipant } from '../participants.js'; +import { PromptRenderer } from '../promptRender.js'; export const EXPORT_QUARTO_COMMAND = 'exportQuarto'; @@ -21,7 +18,7 @@ export async function quartoHandler( response: vscode.ChatResponseStream, token: vscode.CancellationToken ) { - const system = await fs.promises.readFile(`${MD_DIR}/prompts/chat/quarto.md`, 'utf8'); + const system = PromptRenderer.renderCommandPrompt(EXPORT_QUARTO_COMMAND, request, context).content; response.markdown(vscode.l10n.t('Okay!')); response.progress(vscode.l10n.t('Creating new Quarto document...')); @@ -67,7 +64,3 @@ export async function quartoHandler( } } } - -export function registerQuartoCommand() { - PositronAssistantChatParticipant.registerCommand(EXPORT_QUARTO_COMMAND, quartoHandler); -} diff --git a/extensions/positron-assistant/src/completion.ts b/extensions/positron-assistant/src/completion.ts index f1e73b379d77..5b22477b989a 100644 --- a/extensions/positron-assistant/src/completion.ts +++ b/extensions/positron-assistant/src/completion.ts @@ -7,10 +7,11 @@ import * as vscode from 'vscode'; import * as positron from 'positron'; import * as ai from 'ai'; import * as fs from 'fs'; +import * as path from 'path'; import { ModelConfig } from './config'; import { createAnthropic } from '@ai-sdk/anthropic'; -import { MD_DIR } from './constants'; +import { MARKDOWN_DIR } from './constants'; import { createOpenAI } from '@ai-sdk/openai'; import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; @@ -404,7 +405,7 @@ abstract class FimPromptCompletion extends CompletionModel { const signal = controller.signal; token.onCancellationRequested(() => controller.abort()); - const system: string = await fs.promises.readFile(`${MD_DIR}/prompts/completion/fim.md`, 'utf8'); + const system: string = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'completion', 'fim.md'), 'utf8'); const { textStream } = await ai.streamText({ model: this.model, system: system, diff --git a/extensions/positron-assistant/src/constants.ts b/extensions/positron-assistant/src/constants.ts index d1fb52e316ec..92c08ee5bac4 100644 --- a/extensions/positron-assistant/src/constants.ts +++ b/extensions/positron-assistant/src/constants.ts @@ -34,8 +34,5 @@ export const TOOL_TAG_REQUIRES_WORKSPACE = 'requires-workspace'; */ export const TOOL_TAG_REQUIRES_ACTIVE_SESSION = 'requires-session'; -/** The absolute path to the directory containing markdown files (e.g. prompt templates). */ -export const MD_DIR = `${EXTENSION_ROOT_DIR}/src/md/`; - /** Max number of variables to include in language session context */ export const MAX_CONTEXT_VARIABLES = 400; diff --git a/extensions/positron-assistant/src/edits.ts b/extensions/positron-assistant/src/edits.ts index c26c44a98801..95457ed75107 100644 --- a/extensions/positron-assistant/src/edits.ts +++ b/extensions/positron-assistant/src/edits.ts @@ -5,8 +5,9 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; +import * as path from 'path'; -import { MD_DIR } from './constants'; +import { MARKDOWN_DIR } from './constants'; import { ParticipantService } from './participants.js'; type LMTextEdit = { append: string } | { delete: string; replace: string }; @@ -134,7 +135,7 @@ async function* mapEdit( token: vscode.CancellationToken, ) { // Read the system prompt for the language model from the markdown file - const system: string = await fs.promises.readFile(`${MD_DIR}/prompts/chat/mapedit.md`, 'utf8'); + const system: string = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'mapedit.md'), 'utf8'); // Send a request to the language model with the document and code block const response = await model.sendRequest([ diff --git a/extensions/positron-assistant/src/extension.ts b/extensions/positron-assistant/src/extension.ts index ef9527669f29..a111f6affb16 100644 --- a/extensions/positron-assistant/src/extension.ts +++ b/extensions/positron-assistant/src/extension.ts @@ -22,6 +22,7 @@ import { AnthropicLanguageModel } from './anthropic.js'; import { registerParticipantDetectionProvider } from './participantDetection.js'; import { registerAssistantCommands } from './commands/index.js'; import { PositronAssistantApi } from './api.js'; +import { registerPromptManagement } from './promptRender.js'; const hasChatModelsContextKey = 'positron-assistant.hasChatModels'; @@ -296,6 +297,7 @@ function registerAssistant(context: vscode.ExtensionContext) { registerGenerateCommitMessageCommand(context, participantService, log); registerExportChatCommands(context); registerToggleInlineCompletionsCommand(context); + registerPromptManagement(context); // Register mapped edits provider registerMappedEditsProvider(context, participantService, log); diff --git a/extensions/positron-assistant/src/git.ts b/extensions/positron-assistant/src/git.ts index 36641d930139..0f90aa2bcf2d 100644 --- a/extensions/positron-assistant/src/git.ts +++ b/extensions/positron-assistant/src/git.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import { ParticipantService } from './participants.js'; import { API as GitAPI, GitExtension, Repository, Status, Change } from '../../git/src/api/git.js'; -import { MD_DIR } from './constants'; +import { MARKDOWN_DIR } from './constants'; const generatingGitCommitKey = 'positron-assistant.generatingCommitMessage'; @@ -155,7 +155,7 @@ export async function generateCommitMessage( const gitChanges = stagedChanges.length > 0 ? stagedChanges : allChanges; log.trace(`[git] Sending changes ${JSON.stringify(gitChanges)} to model provider.`); - const system: string = await fs.promises.readFile(`${MD_DIR}/prompts/git/commit.md`, 'utf8'); + const system: string = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'git', 'commit.md'), 'utf8'); try { await Promise.all(gitChanges.map(async ({ repo, changes }) => { if (changes.length > 0) { diff --git a/extensions/positron-assistant/src/md/prompts/chat/activation.md b/extensions/positron-assistant/src/md/prompts/chat/activation.md new file mode 100644 index 000000000000..41d85cb6dc69 --- /dev/null +++ b/extensions/positron-assistant/src/md/prompts/chat/activation.md @@ -0,0 +1,16 @@ +--- +mode: + - ask + - edit + - agent + - editor + - notebook +order: 0 +description: Keywords to steer the LLM +--- + +R for Data Science, Tidy Modeling with R, Happy Git with R, Advanced R, tidyverse, ggplot2, tidyr, dplyr, .by, shiny, reactivity, R6, plumber, pak, reticulate, torch, tidymodels, parsnip, quarto, renv, reproducibility, reprex, here::here, Wickham, Bryan, Cheng, Kuhn, Silge, Robinson, Frick, DRY, test fixtures +Python Polars: The Definitive Guide, Janssens, Nieuwdorp, polars, numpy, seaborn, plotnine, shiny for python, great tables, uv, astral, jupyter, notebook +quarto, markdown, yaml, literal programming, pandoc, observable, reactive +Posit, data science, research, knowledge, technical communication, open-source + diff --git a/extensions/positron-assistant/src/md/prompts/chat/agent.md b/extensions/positron-assistant/src/md/prompts/chat/agent.md index 83d94d1ba255..4b7aabd84c6e 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/agent.md +++ b/extensions/positron-assistant/src/md/prompts/chat/agent.md @@ -1,3 +1,8 @@ +--- +mode: agent +order: 50 +description: Prompt for Agent mode +--- You will be given a task that may require editing multiple files and executing code to achieve. diff --git a/extensions/positron-assistant/src/md/prompts/chat/ask.md b/extensions/positron-assistant/src/md/prompts/chat/ask.md index b431f2b9ed23..b03871c86e90 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/ask.md +++ b/extensions/positron-assistant/src/md/prompts/chat/ask.md @@ -1,3 +1,11 @@ +--- +mode: + - ask + - editor + - notebook +order: 50 +description: Prompt for Ask mode +--- You are running in "Ask" mode. diff --git a/extensions/positron-assistant/src/md/prompts/chat/default.md b/extensions/positron-assistant/src/md/prompts/chat/default.md index 767013564e17..21938927775d 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/default.md +++ b/extensions/positron-assistant/src/md/prompts/chat/default.md @@ -1,10 +1,13 @@ - -R for Data Science, Tidy Modeling with R, Happy Git with R, Advanced R, tidyverse, ggplot2, tidyr, dplyr, .by, shiny, reactivity, R6, plumber, pak, reticulate, torch, tidymodels, parsnip, quarto, renv, reproducibility, reprex, here::here, Wickham, Bryan, Cheng, Kuhn, Silge, Robinson, Frick, DRY, test fixtures -Python Polars: The Definitive Guide, Janssens, Nieuwdorp, polars, numpy, seaborn, plotnine, shiny for python, great tables, uv, astral, jupyter, notebook -quarto, markdown, yaml, literal programming, pandoc, observable, reactive -Posit, data science, research, knowledge, technical communication, open-source - - +--- +mode: + - ask + - edit + - agent + - editor + - notebook +order: 10 +description: The default Positron Assistant prompt +--- You are Positron Assistant, a coding assistant designed to help with data science tasks created by Posit, PBC. You are an expert data scientist and software developer, with expertise in R and Python programming. Your job is to assist a USER by answering questions and helping them with their coding and data science tasks. @@ -61,34 +64,3 @@ You do not mention the context in your response if it is irrelevant, but do keep If the USER asks you about features or abilities of the Positron editor that you do not recognize in the automatically provided context, direct the USER to the user guides provided online at . - - -We will provide you with a collection of tools to interact with the current Positron session. - -The USER can see when you invoke a tool, so you do not need to tell the user or mention the name of tools when you use them. - -You prefer to use knowledge you are already provided with to infer details when assisting the USER with their request. You bias to only running tools if it is necessary to learn something in the running Positron session. - -You much prefer to respond to the USER with code to perform a data analysis, rather than directly trying to calculate summaries or statistics for your response. - -Tools with tag `high-token-usage` may result in high token usage, so redirect the USER to provide you with the information you need to answer their question without using these tools whenever possible. For example, if the USER asks about their variables or data: - - When `session` information is not attached to the USER's query, ask the USER to ensure a Console is running and enable the Console session context. - - When file `attachments` are not attached to the USER's query, ask the USER to attach relevant files as context. - - DO NOT construct the project tree, search for text or retrieve file contents using the tools, unless the USER specifically asks you to do so. - - - -When the USER asks a question about Shiny, you attempt to respond as normal in the first instance. - -If you find you cannot complete the USER’s Shiny request or don’t know the answer to their Shiny question, suggest that they use the `@shiny` command in the chat panel to provide additional support using Shiny Assistant. - -If the USER asks you to run or start a Shiny app, you direct them to use the Shiny Assistant, which is able to launch a Shiny app correctly. - - - -When the USER asks a question about Quarto, you attempt to respond as normal in the first instance. - -When you respond with Quarto examples, you use at least four tildes (`~~~~quarto`) to create the surrounding codeblock. - -If you find you cannot complete the USER’s Quarto request, or don’t know the answer to their Quarto question, direct the USER to the user guides provided online at . - diff --git a/extensions/positron-assistant/src/md/prompts/chat/edit.md b/extensions/positron-assistant/src/md/prompts/chat/edit.md index a81973af831d..7366d6715af4 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/edit.md +++ b/extensions/positron-assistant/src/md/prompts/chat/edit.md @@ -1,3 +1,8 @@ +--- +mode: edit +order: 50 +description: Prompt for Edit mode +--- You are running in "Edit" mode. diff --git a/extensions/positron-assistant/src/md/prompts/chat/editor.md b/extensions/positron-assistant/src/md/prompts/chat/editor.md index 57014d57d981..92ce22cbc58b 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/editor.md +++ b/extensions/positron-assistant/src/md/prompts/chat/editor.md @@ -1,9 +1,35 @@ -The user has invoked you from the text editor. They want to change something in -the provided document. Your goal is to generate a set of edits to the document -that represent the requested change. +--- +mode: + - editor + - notebook +order: 70 +description: Prompt when using the inline editor +--- +The user has invoked you from the text editor. -Use the line and column provided to provide the user with a response appropriate -to the current cursor location. Unless otherwise directed, focus on the text -near and below the cursor. +{{@if(positron.request.location2.selection.isEmpty)}} +{{@if(positron.streamingEdits)}} +You may respond in one of three ways: + +1. A BRIEF answer to the user's question -- no `` tag. +2. Return ONLY with `` tags as defined below -- no explanation. + 1. Use a `` tag for each suggested edit. + 2. The `` text MUST be a unique match for the text you wish to replace, including whitespace and indentation. + 3. If there are multiple matches, the first one will be replaced. +3. If you don't know how to answer the user's question, return an empty string. + + +The old text to replace. +The new text to insert in place of the old text. + + +Unless otherwise directed, focus on the text on the line of the cursor position or near to it as determine from the `editor` context. +{{#else}} +They want to change something in the provided document. Your goal is to generate a set of edits to the document that represent the requested change. + +Use the line and column provided to provide the user with a response appropriate to the current cursor location. +Unless otherwise directed, focus on the text near and below the cursor. When you are done, use the provided tool to apply your edits. +{{/if}} +{{/if}} diff --git a/extensions/positron-assistant/src/md/prompts/chat/editorStreaming.md b/extensions/positron-assistant/src/md/prompts/chat/editorStreaming.md deleted file mode 100644 index 8caacf47ad7a..000000000000 --- a/extensions/positron-assistant/src/md/prompts/chat/editorStreaming.md +++ /dev/null @@ -1,17 +0,0 @@ -The user has invoked you from the text editor. - -You may respond in one of three ways: - -1. A BRIEF answer to the user's question -- no `` tag. -2. Return ONLY with `` tags as defined below -- no explanation. - 1. Use a `` tag for each suggested edit. - 2. The `` text MUST be a unique match for the text you wish to replace, including whitespace and indentation. - 3. If there are multiple matches, the first one will be replaced. -3. If you don't know how to answer the user's question, return an empty string. - - -The old text to replace. -The new text to insert in place of the old text. - - -Unless otherwise directed, focus on the text on the line of the cursor position or near to it as determine from the `editor` context. diff --git a/extensions/positron-assistant/src/md/prompts/chat/filepaths.md b/extensions/positron-assistant/src/md/prompts/chat/filepaths.md index 8d9e71928717..c489ad4244e2 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/filepaths.md +++ b/extensions/positron-assistant/src/md/prompts/chat/filepaths.md @@ -1,3 +1,12 @@ +--- +mode: + - ask + - edit + - agent +order: 25 +description: Instructions for using file paths +--- + When the user describes a file in the project or mentions a file name, you may need to invoke a tool to determine the path to the file, such as the project tree tool. Although file names may provide some context, they are not sufficient to determine the purpose of the file. Therefore, you should not use file names to infer the file type. Instead, you should rely on the file extension or the content of the file to determine its purpose. @@ -19,7 +28,10 @@ Common mistakes to avoid: - Never include the workspace root in relative paths - Never add a leading slash to relative paths + Example transformations: /home/user/workspace/docs/readme.md → docs/readme.md /home/user/workspace/src/lib/utils.py → src/lib/utils.py /home/user/external/config.json → /home/user/external/config.json + + diff --git a/extensions/positron-assistant/src/md/prompts/chat/fix.md b/extensions/positron-assistant/src/md/prompts/chat/fix.md deleted file mode 100644 index 0f587862a85c..000000000000 --- a/extensions/positron-assistant/src/md/prompts/chat/fix.md +++ /dev/null @@ -1,15 +0,0 @@ -The user has executed code in the Console, and expects you to propose a fix for one or more problems in that code. If the user provides a specific error message or description of the issue, only attempt to fix that problem. - -The error code may originate in a file on disk. Use the attached interpreter session context, and optionally the `getProjectTree` tool, to locate the path to the file on disk. - -Your response must conform to this Markdown example: -````markdown -One or two sentence description of the fix. - -```${language} -${fixedCode} -``` -```` - -Rules: - - If a file was identified, include a valid, clickable Markdown link to that file in the response. Ensure the link uses the absolute path to that file. diff --git a/extensions/positron-assistant/src/md/prompts/chat/fixEditor.md b/extensions/positron-assistant/src/md/prompts/chat/fixEditor.md deleted file mode 100644 index 06ae0e2f6e85..000000000000 --- a/extensions/positron-assistant/src/md/prompts/chat/fixEditor.md +++ /dev/null @@ -1,5 +0,0 @@ -The user has troublesome code in the Editor, and expects you to propose a fix for one or more problems in that code. If the user provides a specific error message or description of the issue, focus on fixing that problem. - -The troublesome code may have errors such as spelling mistakes, bad names, etc. **Do not** attempt to fix those; only fix code that is syntactically or semantically incorrect. Use attached diagnostics information to identify the specific issues, fixing only diagnostics of Error and Warning levels. - -Provide a one or two sentence description of the fix, and then the code changes. diff --git a/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md b/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md index f880d67066e0..99ee927e2c02 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md +++ b/extensions/positron-assistant/src/md/prompts/chat/instructions-python.md @@ -1,3 +1,12 @@ +--- +mode: + - ask + - edit + - agent +order: 100 +description: Prompt for when a Python session is running +--- +{{@if(positron.sessions.map(function(s){return s.languageId}).includes("python"))}} You write clean, efficient and maintainable Python code. @@ -432,3 +441,4 @@ p_bar = ( p_bar.show() ``` +{{/if}} diff --git a/extensions/positron-assistant/src/md/prompts/chat/instructions-r.md b/extensions/positron-assistant/src/md/prompts/chat/instructions-r.md index d24627d25190..05eb49c49958 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/instructions-r.md +++ b/extensions/positron-assistant/src/md/prompts/chat/instructions-r.md @@ -1,3 +1,12 @@ +--- +mode: + - ask + - edit + - agent +order: 100 +description: Prompt for when an R session is running +--- +{{@if(positron.sessions.map(function(s){return s.languageId}).includes("r"))}} When writing R code you generally follow tidyverse coding style and principles. @@ -271,3 +280,4 @@ diamonds_summary_numeric <- diamonds |> ) ``` +{{/if}} diff --git a/extensions/positron-assistant/src/md/prompts/chat/participants.md b/extensions/positron-assistant/src/md/prompts/chat/participants.md new file mode 100644 index 000000000000..1f24050c8e26 --- /dev/null +++ b/extensions/positron-assistant/src/md/prompts/chat/participants.md @@ -0,0 +1,15 @@ +--- +mode: + - ask + - edit + - agent +order: 30 +description: Instructions for the @shiny chat participant +--- + +When the USER asks a question about Shiny, you attempt to respond as normal in the first instance. + +If you find you cannot complete the USER’s Shiny request or don’t know the answer to their Shiny question, suggest that they use the `@shiny` command in the chat panel to provide additional support using Shiny Assistant. + +If the USER asks you to run or start a Shiny app, you direct them to use the Shiny Assistant, which is able to launch a Shiny app correctly. + diff --git a/extensions/positron-assistant/src/md/prompts/chat/quarto.md b/extensions/positron-assistant/src/md/prompts/chat/quarto.md index 6a8f329a3a25..a156ae9e8e02 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/quarto.md +++ b/extensions/positron-assistant/src/md/prompts/chat/quarto.md @@ -1,3 +1,15 @@ -Take the full conversation so far and convert it into a complete quarto document. -Output ONLY the contents of the `.Qmd` file, nothing else, and do not wrap the output in markdown tags. -Expand on details in the text of the report, include plots and tables where relevant. +--- +mode: + - ask + - edit + - agent +order: 40 +description: Prompting related to Quarto +--- + +When the USER asks a question about Quarto, you attempt to respond as normal in the first instance. + +When you respond with Quarto examples, you use at least four tildes (`~~~~quarto`) to create the surrounding codeblock. + +If you find you cannot complete the USER’s Quarto request, or don’t know the answer to their Quarto question, direct the USER to the user guides provided online at . + diff --git a/extensions/positron-assistant/src/md/prompts/chat/selection.md b/extensions/positron-assistant/src/md/prompts/chat/selection.md index 4346009a175e..c89202efdbb2 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/selection.md +++ b/extensions/positron-assistant/src/md/prompts/chat/selection.md @@ -1,12 +1,28 @@ -The user has invoked you from the text editor. +--- +mode: + - editor + - notebook +order: 70 +description: Prompt for when text is selected in Edit mode +--- +{{@if(!positron.request.location2.selection.isEmpty)}} +{{@if(positron.streamingEdits)}} +You may respond in one of three ways: +1. A BRIEF answer to the user's question. +2. Return ONLY a single `` tag as defined below -- no explanation. +3. If you don't know how to answer the user's question, return an empty string. + +The new text to insert in place of the selection. + +Unless otherwise directed, focus on the selected text in the `editor` context. +{{#else}} When you have finished responding, you can choose to output a revised version of the selection provided by the user if required. Never mention the name of the function, just use it. -If there is selected text, assume the user has a question about it or wants to -replace it with something else. +If there is selected text, assume the user has a question about it or wants to replace it with something else. -Use the line and column provided to provide the user with response appropriate -to the current cursor location, but don't mention the line and column numbers -in your response unless needed for clarification. +Use the line and column provided to provide the user with response appropriate to the current cursor location, but don't mention the line and column numbers in your response unless needed for clarification. +{{/if}} +{{/if}} diff --git a/extensions/positron-assistant/src/md/prompts/chat/selectionStreaming.md b/extensions/positron-assistant/src/md/prompts/chat/selectionStreaming.md deleted file mode 100644 index 426154374674..000000000000 --- a/extensions/positron-assistant/src/md/prompts/chat/selectionStreaming.md +++ /dev/null @@ -1,11 +0,0 @@ -The user has invoked you from the text editor. - -You may respond in one of three ways: - -1. A BRIEF answer to the user's question. -2. Return ONLY a single `` tag as defined below -- no explanation. -3. If you don't know how to answer the user's question, return an empty string. - -The new text to insert in place of the selection. - -Unless otherwise directed, focus on the selected text in the `editor` context. diff --git a/extensions/positron-assistant/src/md/prompts/chat/terminal.md b/extensions/positron-assistant/src/md/prompts/chat/terminal.md index 670fde31fcd5..3c2f31bfe7d5 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/terminal.md +++ b/extensions/positron-assistant/src/md/prompts/chat/terminal.md @@ -1,3 +1,8 @@ +--- +mode: terminal +order: 10 +description: Prompt used when in the Terminal +--- You may respond in one of two ways: 1. Answer the user's question in 1-3 brief sentences. diff --git a/extensions/positron-assistant/src/md/prompts/chat/tools.md b/extensions/positron-assistant/src/md/prompts/chat/tools.md new file mode 100644 index 000000000000..593fcfc7c195 --- /dev/null +++ b/extensions/positron-assistant/src/md/prompts/chat/tools.md @@ -0,0 +1,22 @@ +--- +mode: + - ask + - edit + - agent +order: 20 +description: Instructions for using tools +--- + +We will provide you with a collection of tools to interact with the current Positron session. + +The USER can see when you invoke a tool, so you do not need to tell the user or mention the name of tools when you use them. + +You prefer to use knowledge you are already provided with to infer details when assisting the USER with their request. You bias to only running tools if it is necessary to learn something in the running Positron session. + +You much prefer to respond to the USER with code to perform a data analysis, rather than directly trying to calculate summaries or statistics for your response. + +Tools with tag `high-token-usage` may result in high token usage, so redirect the USER to provide you with the information you need to answer their question without using these tools whenever possible. For example, if the USER asks about their variables or data: + - When `session` information is not attached to the USER's query, ask the USER to ensure a Console is running and enable the Console session context. + - When file `attachments` are not attached to the USER's query, ask the USER to attach relevant files as context. + - DO NOT construct the project tree, search for text or retrieve file contents using the tools, unless the USER specifically asks you to do so. + diff --git a/extensions/positron-assistant/src/md/prompts/chat/warning.md b/extensions/positron-assistant/src/md/prompts/chat/warning.md index fe36ba5e90b7..3ed33003826b 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/warning.md +++ b/extensions/positron-assistant/src/md/prompts/chat/warning.md @@ -1,13 +1,35 @@ +--- +mode: + - ask + - edit + - agent + - editor + - notebook +order: 60 +description: Instructions for issuing warnings +--- +{{@if(positron.context.participantId === "positron.assistant.editor")}} +If the suggested edit includes destructive, dangerous, or difficult to reverse actions, you follow these guidelines: +{{#else}} When responding with code or instructions that are destructive, dangerous, or difficult to reverse, you follow these guidelines: +{{/if}} + - **Always include warnings** for these specific operations: - Deleting files or directories (`rm`, `os.remove()`, `unlink()`, `fs.unlink()`, etc.) - Modifying system files or directories -- Start with a clear warning at the beginning of the response -- Include additional warnings alongside the code or instructions where appropriate - Enclose the warning text in `` tags. For example: `**Warning: This code will permanently delete the current directory and all its contents. Use with caution!**` - The warning text should clearly describe the destructive or dangerous nature of the suggested action or code +{{@if(positron.context.participantId !== "positron.assistant.editor")}} +- Start with a clear warning at the beginning of the response +- Include additional warnings alongside the code or instructions where appropriate +{{/if}} +{{@if(positron.context.participantId === "positron.assistant.editor")}} + +**Warning: This code will permanently delete the directory and all its contents. Use with caution!** + +{{#else}} delete a directory using Python @@ -26,4 +48,5 @@ shutil.rmtree('/path/to/directory') ```` +{{/if}} diff --git a/extensions/positron-assistant/src/md/prompts/chat/warningStreaming.md b/extensions/positron-assistant/src/md/prompts/chat/warningStreaming.md deleted file mode 100644 index 45e9276be149..000000000000 --- a/extensions/positron-assistant/src/md/prompts/chat/warningStreaming.md +++ /dev/null @@ -1,10 +0,0 @@ -If the suggested edit includes destructive, dangerous, or difficult to reverse actions, you follow these guidelines: -- **Always include warnings** for these specific operations: - - Deleting files or directories (`rm`, `os.remove()`, `unlink()`, `fs.unlink()`, etc.) - - Modifying system files or directories -- Enclose the warning text in `` tags. For example: `**Warning: This code will permanently delete the current directory and all its contents. Use with caution!**` -- The warning text should clearly describe the destructive or dangerous nature of the suggested action or code - - -**Warning: This code will permanently delete the directory and all its contents. Use with caution!** - diff --git a/extensions/positron-assistant/src/md/prompts/chat/doc.md b/extensions/positron-assistant/src/md/prompts/commands/doc.md similarity index 93% rename from extensions/positron-assistant/src/md/prompts/chat/doc.md rename to extensions/positron-assistant/src/md/prompts/commands/doc.md index f21d386b150b..5350f8b45d06 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/doc.md +++ b/extensions/positron-assistant/src/md/prompts/commands/doc.md @@ -1,3 +1,7 @@ +--- +command: doc +mode: editor +--- The user has asked you to generate documentation for the code in the Editor. Use the information in the code comments and the overall structure of the code to create comprehensive documentation. Be succinct but useful in your documentation. Follow standard documentation guidelines for language in which the code is written. If multiple documentation styles exist, use the one that already exists in the code. Respond with the code changes to add the documentation. You may provide at most one sentence of narrative. diff --git a/extensions/positron-assistant/src/md/prompts/chat/explain.md b/extensions/positron-assistant/src/md/prompts/commands/explain.md similarity index 88% rename from extensions/positron-assistant/src/md/prompts/chat/explain.md rename to extensions/positron-assistant/src/md/prompts/commands/explain.md index 82a28cc02a0b..8055f1114359 100644 --- a/extensions/positron-assistant/src/md/prompts/chat/explain.md +++ b/extensions/positron-assistant/src/md/prompts/commands/explain.md @@ -1,7 +1,17 @@ +--- +command: explain +mode: + - ask + - editor +--- You are a world-class coding tutor. Your code explanations perfectly balance high-level concepts and granular details. Your approach ensures that students not only understand how to write code, but also grasp the underlying principles that guide effective programming. ## Task +{{@if(positron.context.participantId === "positron.assistant.chat")}} The user has attached a file to the chat. Explain the code in the file and how it relates to the user's question. Be sure to follow the rules. +{{#else}} +Answer the user's question. Be sure to follow the rules. +{{/if}} ## Rules - Think step by step: diff --git a/extensions/positron-assistant/src/md/prompts/commands/fix.md b/extensions/positron-assistant/src/md/prompts/commands/fix.md new file mode 100644 index 000000000000..8dae8ad6efae --- /dev/null +++ b/extensions/positron-assistant/src/md/prompts/commands/fix.md @@ -0,0 +1,32 @@ +--- +command: fix +mode: + - ask + - editor +--- + +The user has {{@if(positron.context.participantId === "positron.assistant.chat")}}executed code in the Console, {{#else}}troublesome code in the Editor, {{/if}}and expects you to propose a fix for one or more problems in that code. If the user provides a specific error message or description of the issue, focus on fixing that problem. + +{{@if(positron.context.participantId === "positron.assistant.chat")}} +The error code may originate in a file on disk. Use the attached interpreter session context, and optionally the `getProjectTree` tool, to locate the path to the file on disk. +{{#else}} +Use attached diagnostics information to identify the specific issues, fixing only diagnostics of Error and Warning levels. +{{/if}} + +The troublesome code may have errors such as spelling mistakes, bad names, etc. **Do not** attempt to fix those; only fix code that is syntactically or semantically incorrect. + +Provide a one or two sentence description of the fix, and then the code changes. + +{{@if(positron.context.participantId === "positron.assistant.chat")}} +Your response must conform to this Markdown example: +````markdown +One or two sentence description of the fix. + +```${language} +${fixedCode} +``` +```` + +Rules: + - If a file was identified, include a valid, clickable Markdown link to that file in the response. Ensure the link uses the absolute path to that file. +{{/if}} diff --git a/extensions/positron-assistant/src/md/prompts/commands/quarto.md b/extensions/positron-assistant/src/md/prompts/commands/quarto.md new file mode 100644 index 000000000000..9b01f98181bd --- /dev/null +++ b/extensions/positron-assistant/src/md/prompts/commands/quarto.md @@ -0,0 +1,7 @@ +--- +command: exportQuarto +mode: ask +--- +Take the full conversation so far and convert it into a complete quarto document. +Output ONLY the contents of the `.Qmd` file, nothing else, and do not wrap the output in markdown tags. +Expand on details in the text of the report, include plots and tables where relevant. diff --git a/extensions/positron-assistant/src/md/prompts/completion/fim.md b/extensions/positron-assistant/src/md/prompts/completion/fim.md index be07a3ab0167..3978e63a094a 100644 --- a/extensions/positron-assistant/src/md/prompts/completion/fim.md +++ b/extensions/positron-assistant/src/md/prompts/completion/fim.md @@ -41,5 +41,3 @@ baz = 789 qux = foo + bar + baz - -The next messages you see will be from the user. diff --git a/extensions/positron-assistant/src/participants.ts b/extensions/positron-assistant/src/participants.ts index 667fca891a36..20205a74a9ec 100644 --- a/extensions/positron-assistant/src/participants.ts +++ b/extensions/positron-assistant/src/participants.ts @@ -10,7 +10,7 @@ import * as fs from 'fs'; import * as xml from './xml.js'; import { MARKDOWN_DIR, MAX_CONTEXT_VARIABLES } from './constants'; -import { isChatImageMimeType, isTextEditRequest, isWorkspaceOpen, languageModelCacheBreakpointPart, toLanguageModelChatMessage, uriToString, isRuntimeSessionReference } from './utils'; +import { isChatImageMimeType, isTextEditRequest, languageModelCacheBreakpointPart, toLanguageModelChatMessage, uriToString, isRuntimeSessionReference, isPromptInstructionsReference } from './utils'; import { ContextInfo, PositronAssistantToolName } from './types.js'; import { DefaultTextProcessor } from './defaultTextProcessor.js'; import { ReplaceStringProcessor } from './replaceStringProcessor.js'; @@ -20,6 +20,7 @@ import { IChatRequestHandler } from './commands/index.js'; import { getCommitChanges } from './git.js'; import { getEnabledTools, getPositronContextPrompts } from './api.js'; import { TokenUsage } from './tokens.js'; +import { PromptRenderer } from './promptRender.js'; export enum ParticipantID { /** The participant used in the chat pane in Ask mode. */ @@ -126,7 +127,7 @@ export interface PositronAssistantChatContext extends vscode.ChatContext { participantId: ParticipantID; /** The system prompt to use for the participant. */ - systemPrompt: string; + systemPrompt?: string; /** The tools allowed for the participant. */ toolAvailability: Map; @@ -252,9 +253,6 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici incomingContext: vscode.ChatContext, response: vscode.ChatResponseStream ): Promise { - // System prompt - const systemPrompt = await this.getSystemPrompt(request); - // Get the IDE context for the request. const positronContext = await positron.ai.getPositronChatContext(request); @@ -271,7 +269,7 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici ...incomingContext, participantId: this.id, positronContext, - systemPrompt, + systemPrompt: request.modeInstructions, // Start with the default Code OSS "mode instructions" toolAvailability, contextInfo: undefined, async attachContextInfo(messages: vscode.LanguageModelChatMessage2[]) { @@ -287,6 +285,9 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici } }; + // Append this participant's additional system prompt + assistantContext.systemPrompt ??= ''; + assistantContext.systemPrompt += await this.getSystemPrompt(request, assistantContext); return assistantContext; } @@ -310,12 +311,14 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici // Construct the transient message thread sent to the language model. // Note that this is not the same as the chat history shown in the UI. - // Start with the chat history. + // Start with the system prompt if available. + const messages: vscode.LanguageModelChatMessage2[] = systemPrompt ? [ + new vscode.LanguageModelChatMessage(vscode.LanguageModelChatMessageRole.System, systemPrompt) + ] : []; + + // Add the current chat history. // Note that context.history excludes tool calls and results. - const messages = [ - new vscode.LanguageModelChatMessage(vscode.LanguageModelChatMessageRole.System, systemPrompt), - ...toLanguageModelChatMessage(context.history), - ]; + messages.push(...toLanguageModelChatMessage(context.history)); // Add cache breakpoints to at-most the last 2 user messages. addCacheControlBreakpointPartsToLastUserMessages(messages, 2); @@ -349,7 +352,7 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici }; } - protected abstract getSystemPrompt(request: vscode.ChatRequest): Promise; + protected abstract getSystemPrompt(request: vscode.ChatRequest, assistantContext: PositronAssistantChatContext): Promise; protected mapDiagnostics(diagnostics: vscode.Diagnostic[], selection?: vscode.Position | vscode.Range | vscode.Selection): string { const severityMap = { @@ -383,21 +386,6 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici // 2. Binary data (e.g. image attachments). const userDataParts: vscode.LanguageModelDataPart[] = []; - // If the workspace has an llms.txt document, add it's current value to the prompt. - const llmsDocument = await openLlmsTextDocument(); - if (llmsDocument) { - const llmsText = llmsDocument.getText(); - if (llmsText.trim() !== '') { - // Add the file as a reference in the response. - response.reference(llmsDocument.uri); - - // Add the contents of the file to the prompt - const instructionsNode = xml.node('instructions', llmsText); - prompts.push(instructionsNode); - log.debug(`[context] adding llms.txt context: ${llmsText.length} characters`); - } - } - // If the user has explicitly attached a tool reference, add it to the prompt. if (request.toolReferences.length > 0) { const referencePrompts: string[] = []; @@ -411,6 +399,7 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici // If the user has explicitly attached files as context, add them to the prompt. if (request.references.length > 0) { + const instructionPrompts: string[] = []; const attachmentPrompts: string[] = []; const sessionPrompts: string[] = []; for (const reference of request.references) { @@ -431,6 +420,12 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici }); sessionPrompts.push(xml.node('session', sessionContent)); log.debug(`[context] adding session context for session ${value.activeSession!.identifier}: ${sessionContent.length} characters`); + } else if (isPromptInstructionsReference(reference)) { + // A prompt instructions file has automatically been attached + response.reference(reference.value); + const document = await vscode.workspace.openTextDocument(reference.value); + const documentText = document.getText(); + instructionPrompts.push(documentText); } else if (value instanceof vscode.Location) { // The user attached a range of a file - // usually the automatically attached visible region of the active file. @@ -564,6 +559,11 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici const sessionContent = `${sessionText}\n${sessionPrompts.join('\n')}`; prompts.push(xml.node('sessions', sessionContent)); } + + if (instructionPrompts.length > 0) { + // Add instructions files to the prompt. + prompts.push(xml.node('instructions', instructionPrompts.join('\n'))); + } } // Add Positron IDE context to the prompt. @@ -721,24 +721,6 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici return defaultTextProcessor; } - /** Additional language-specific prompts for active sessions */ - protected async getActiveSessionInstructions(): Promise { - const sessions = await positron.runtime.getActiveSessions(); - const languages = sessions.map((session) => session.runtimeMetadata.languageId); - - const instructions = await Promise.all(languages.map(async (id) => { - try { - const instructions = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', `instructions-${id}.md`), 'utf8'); - return instructions + '\n\n'; - } catch { - // There are no additional instructions for this language ID - return ''; - } - })); - - return instructions.join(''); - } - dispose(): void { } } @@ -746,29 +728,23 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici export class PositronAssistantChatParticipant extends PositronAssistantParticipant implements IPositronAssistantParticipant { id = ParticipantID.Chat; - protected override async getSystemPrompt(request: vscode.ChatRequest): Promise { - const defaultSystem = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'default.md'), 'utf8'); - const filepaths = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'filepaths.md'), 'utf8'); - const ask = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'ask.md'), 'utf8'); - const languages = await this.getActiveSessionInstructions(); - const warning = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'warning.md'), 'utf8'); - const prompts = [defaultSystem, filepaths, warning, ask, languages]; - return prompts.join('\n\n'); + protected override async getSystemPrompt(request: vscode.ChatRequest, context: PositronAssistantChatContext): Promise { + const activeSessions = await positron.runtime.getActiveSessions(); + const sessions = activeSessions.map(session => session.runtimeMetadata); + const prompt = PromptRenderer.renderModePrompt(positron.PositronChatMode.Ask, { request, context, sessions }); + return prompt.content; } } /** The participant used in the chat pane in Edit mode. */ -class PositronAssistantEditParticipant extends PositronAssistantParticipant implements IPositronAssistantParticipant { +export class PositronAssistantEditParticipant extends PositronAssistantParticipant implements IPositronAssistantParticipant { id = ParticipantID.Edit; - protected override async getSystemPrompt(request: vscode.ChatRequest): Promise { - const defaultSystem = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'default.md'), 'utf8'); - const filepaths = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'filepaths.md'), 'utf8'); - const edit = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'edit.md'), 'utf8'); - const languages = await this.getActiveSessionInstructions(); - const warning = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'warning.md'), 'utf8'); - const prompts = [defaultSystem, filepaths, warning, edit, languages]; - return prompts.join('\n\n'); + protected override async getSystemPrompt(request: vscode.ChatRequest, context: PositronAssistantChatContext): Promise { + const activeSessions = await positron.runtime.getActiveSessions(); + const sessions = activeSessions.map(session => session.runtimeMetadata); + const prompt = PromptRenderer.renderModePrompt(positron.PositronChatMode.Edit, { request, context, sessions }); + return prompt.content; } } @@ -776,24 +752,24 @@ class PositronAssistantEditParticipant extends PositronAssistantParticipant impl export class PositronAssistantAgentParticipant extends PositronAssistantParticipant implements IPositronAssistantParticipant { id = ParticipantID.Agent; - protected override async getSystemPrompt(request: vscode.ChatRequest): Promise { - const defaultSystem = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'default.md'), 'utf8'); - const filepaths = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'filepaths.md'), 'utf8'); - const agent = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'agent.md'), 'utf8'); - const languages = await this.getActiveSessionInstructions(); - const warning = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'warning.md'), 'utf8'); - const prompts = [defaultSystem, filepaths, warning, agent, languages]; - return prompts.join('\n\n'); + protected override async getSystemPrompt(request: vscode.ChatRequest, context: PositronAssistantChatContext): Promise { + const activeSessions = await positron.runtime.getActiveSessions(); + const sessions = activeSessions.map(session => session.runtimeMetadata); + const prompt = PromptRenderer.renderModePrompt(positron.PositronChatMode.Agent, { request, context, sessions }); + return prompt.content; } } /** The participant used in terminal inline chats. */ -class PositronAssistantTerminalParticipant extends PositronAssistantParticipant implements IPositronAssistantParticipant { +export class PositronAssistantTerminalParticipant extends PositronAssistantParticipant implements IPositronAssistantParticipant { id = ParticipantID.Terminal; - protected override async getSystemPrompt(request: vscode.ChatRequest): Promise { + protected override async getSystemPrompt(request: vscode.ChatRequest, context: PositronAssistantChatContext): Promise { // The terminal prompt includes how to handle warnings in the response. - return await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'terminal.md'), 'utf8'); + const activeSessions = await positron.runtime.getActiveSessions(); + const sessions = activeSessions.map(session => session.runtimeMetadata); + const prompt = PromptRenderer.renderModePrompt(positron.PositronChatAgentLocation.Terminal, { request, context, sessions }); + return prompt.content; } } @@ -801,40 +777,14 @@ class PositronAssistantTerminalParticipant extends PositronAssistantParticipant export class PositronAssistantEditorParticipant extends PositronAssistantParticipant implements IPositronAssistantParticipant { id = ParticipantID.Editor; - protected override async getSystemPrompt(request: vscode.ChatRequest): Promise { + protected override async getSystemPrompt(request: vscode.ChatRequest, context: PositronAssistantChatContext): Promise { if (!isTextEditRequest(request)) { throw new Error(`Editor participant only supports editor requests. Got: ${typeof request.location2}`); } - if (isStreamingEditsEnabled()) { - const warningStreaming = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'warningStreaming.md'), 'utf8'); - - // If the user has not selected text, use the prompt for the whole document. - if (request.location2.selection.isEmpty) { - const editorSteaming = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'editorStreaming.md'), 'utf8'); - return [editorSteaming, warningStreaming].join('\n\n'); - } - - // If the user has selected text, generate a new version of the selection. - const selectionStreaming = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'selectionStreaming.md'), 'utf8'); - return [selectionStreaming, warningStreaming].join('\n\n'); - } - - const defaultSystem = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'default.md'), 'utf8'); - const warning = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'warning.md'), 'utf8'); - - // Inline editor chats behave as in "Ask" mode - const ask = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'ask.md'), 'utf8'); - - // If the user has not selected text, use the prompt for the whole document. - if (request.location2.selection.isEmpty) { - const editor = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'editor.md'), 'utf8'); - return [defaultSystem, warning, ask, editor].join('\n\n'); - } - - // If the user has selected text, generate a new version of the selection. - const selection = await fs.promises.readFile(path.join(MARKDOWN_DIR, 'prompts', 'chat', 'selection.md'), 'utf8'); - return [defaultSystem, warning, ask, selection].join('\n\n'); + const streamingEdits = isStreamingEditsEnabled(); + const prompt = PromptRenderer.renderModePrompt(positron.PositronChatAgentLocation.Editor, { request, context, streamingEdits }); + return prompt.content; } async getCustomPrompt(request: vscode.ChatRequest): Promise { @@ -888,7 +838,7 @@ export class PositronAssistantEditorParticipant extends PositronAssistantPartici } /** The participant used in notebook inline chats. */ -class PositronAssistantNotebookParticipant extends PositronAssistantEditorParticipant implements IPositronAssistantParticipant { +export class PositronAssistantNotebookParticipant extends PositronAssistantEditorParticipant implements IPositronAssistantParticipant { id = ParticipantID.Notebook; // For now, the Notebook Participant inherits everything from the Editor Participant. } diff --git a/extensions/positron-assistant/src/promptRender.ts b/extensions/positron-assistant/src/promptRender.ts new file mode 100644 index 000000000000..b50491a36516 --- /dev/null +++ b/extensions/positron-assistant/src/promptRender.ts @@ -0,0 +1,396 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as yaml from 'yaml'; +import * as Sqrl from 'squirrelly'; +import { MARKDOWN_DIR } from './constants'; +import { log } from './extension.js'; + +const PROMPT_MODE_SELECTIONS_KEY = 'positron.assistant.promptModeSelections'; + +type StoredPromptSelectionConfig = Partial>; + +//#region Prompt templating + +/** + * YAML frontmatter metadata for prompt files + */ +interface PromptMetadata { + description?: string; + mode?: T; + tools?: string[]; + command?: string; + order?: number; +} + +/** Possible vales for the `mode` prompt metadata property */ +type PromptMetadataMode = positron.PositronChatMode | positron.PositronChatAgentLocation; + +/** + * Parsed prompt document + */ +interface ParsedPromptDocument { + metadata: PromptMetadata; + content: string; + filePath: string; +} + +/** + * Parsed & merged command prompt document + */ +interface PromptDocument { + metadata: PromptMetadata; + content: string; +} + +/** + * Metadata for the `positron` data object passed to prompt templates + */ +interface PromptRenderData { + context?: vscode.ChatContext; + request?: vscode.ChatRequest; + document?: vscode.TextDocument; + sessions?: Array; + streamingEdits?: boolean; +} + +export class PromptRenderer { + private static _instance: PromptRenderer | undefined; + constructor(public extensionContext: vscode.ExtensionContext) { + if (!PromptRenderer._instance) { + PromptRenderer._instance = this; + } + } + + /** + * Get the singleton instance of PromptRenderer + */ + static get instance(): PromptRenderer { + if (!PromptRenderer._instance) { + throw new Error('PromptRenderer has not been initialized'); + } + return PromptRenderer._instance; + } + + /** + * Parse YAML frontmatter from markdown content + */ + private parseYamlFrontmatter(content: string): PromptDocument { + const yamlMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + + if (!yamlMatch) { + return { metadata: {}, content }; + } + + const yamlContent = yamlMatch[1]; + const markdownContent = yamlMatch[2]; + + try { + const metadata = yaml.parse(yamlContent) as PromptMetadata; + return { metadata: metadata || {}, content: markdownContent }; + } catch (error) { + log.warn('[PromptRender] Failed to parse YAML frontmatter:', error); + return { metadata: {}, content: markdownContent }; + } + } + + + /** + * Recursively find all .md files in a directory + */ + private findMarkdownFiles(dir: string): string[] { + const files: string[] = []; + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...this.findMarkdownFiles(fullPath)); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(fullPath); + } + } + } catch (error) { + log.warn('[PromptRender] Cannot read prompt files from prompt directory:', error); + } + + return files; + } + + /** + * Load and parse all prompt documents + */ + private loadPromptDocuments(promptsDir: string): ParsedPromptDocument[] { + const documents: ParsedPromptDocument[] = []; + const markdownFiles = this.findMarkdownFiles(promptsDir); + + for (const filePath of markdownFiles) { + try { + const fileContent = fs.readFileSync(filePath, 'utf8'); + const { metadata, content } = this.parseYamlFrontmatter(fileContent); + documents.push({ metadata, content, filePath }); + } catch (error) { + log.warn(`[PromptRender] Failed to load prompt file ${filePath}:`, error); + } + } + + return documents; + } + + /** + * Merge metadata from multiple documents + */ + private mergeMetadata(documents: ParsedPromptDocument[]) { + const merged: PromptMetadata = {}; + const allTools = new Set(); + const allModes = new Set(); + + for (const doc of documents) { + // Combine tools arrays + if (doc.metadata.tools) { + doc.metadata.tools.forEach(tool => allTools.add(tool)); + } + + // Combine modes + if (doc.metadata.mode && typeof doc.metadata.mode === 'string') { + allModes.add(doc.metadata.mode); + } else if (doc.metadata.mode) { + doc.metadata.mode.forEach(mode => allModes.add(mode)); + } + + // Set command + if (doc.metadata.command) { + merged.command = doc.metadata.command; + } + } + + merged.tools = Array.from(allTools); + merged.mode = Array.from(allModes); + + return merged; + } + + /** + * Merge content from multiple documents + */ + private mergeContent(documents: ParsedPromptDocument[]): string { + return documents.map(doc => doc.content.trim()).join('\n\n'); + } + + /** + * Get combined prompt metadata for a specific command + */ + static getCommandMetadata(command: string) { + return PromptRenderer.instance._getCommandMetadata(command); + } + + private _getCommandMetadata(command: string) { + const commandsPath = path.join(MARKDOWN_DIR, 'prompts', 'commands'); + const documents = this.loadPromptDocuments(commandsPath); + const matchingDocuments: ParsedPromptDocument[] = []; + for (const doc of documents) { + if (doc.metadata.command === command) { + matchingDocuments.push(doc); + } + } + + if (matchingDocuments.length === 0) { + throw new Error(`No prompt documents found for command: ${command}`); + } + return this.mergeMetadata(matchingDocuments); + } + + /** + * Get combined prompt for a specific command + */ + static renderCommandPrompt(command: string, request: vscode.ChatRequest, context: vscode.ChatContext): PromptDocument { + return PromptRenderer.instance._renderCommandPrompt(command, request, context); + } + + private _renderCommandPrompt(command: string, request: vscode.ChatRequest, context: vscode.ChatContext): PromptDocument { + const commandsPath = path.join(MARKDOWN_DIR, 'prompts', 'commands'); + const documents = this.loadPromptDocuments(commandsPath); + const matchingDocuments: ParsedPromptDocument[] = []; + for (const doc of documents) { + if (doc.metadata.command === command) { + matchingDocuments.push(doc); + } + } + + if (matchingDocuments.length === 0) { + throw new Error(`No prompt documents found for command: ${command}`); + } + + // Merge prompts + const mergedContent = this.mergeContent(matchingDocuments); + const mergedMetadata = this.mergeMetadata(matchingDocuments); + + // Render prompt template + const data: PromptRenderData = { context, request }; + log.trace('[PromptRender] Rendering prompt for command:', command, 'with data:', JSON.stringify(data)); + const result = Sqrl.render(mergedContent, data, { varName: 'positron' }); + + return { + content: result, + metadata: mergedMetadata, + }; + } + + /** + * Get all prompt documents for a specific mode, optionally filtering by saved selections + */ + getModePromptDocuments(mode: PromptMetadataMode, fromSaved: boolean = true): ParsedPromptDocument[] { + const dir = path.join(MARKDOWN_DIR, 'prompts', 'chat'); + const documents = this.loadPromptDocuments(dir); + + // Read saved selections from storage + const savedSelections = this.extensionContext.globalState?.get(PROMPT_MODE_SELECTIONS_KEY) || {}; + const selections = savedSelections[mode]; + + const matchingDocuments: ParsedPromptDocument[] = documents + .filter(doc => doc.metadata.mode === mode || (Array.isArray(doc.metadata.mode) && doc.metadata.mode.includes(mode))) + .filter(doc => { + const selection = selections?.find(s => s.file === path.basename(doc.filePath)); + return (fromSaved && selection) ? selection.enabled : true; + }); + + // Sort entries by order metadata + matchingDocuments.sort((a, b) => (a.metadata.order ?? 0) - (b.metadata.order ?? 0)); + + return matchingDocuments; + } + + /** + * Get combined prompt for a specific command + */ + static renderModePrompt(mode: PromptMetadataMode, data: PromptRenderData): PromptDocument { + return PromptRenderer.instance._renderModePrompt(mode, data); + } + + private _renderModePrompt(mode: PromptMetadataMode, data: PromptRenderData): PromptDocument { + const matchingDocuments = this.getModePromptDocuments(mode); + if (matchingDocuments.length === 0) { + return { content: '', metadata: {} }; + } + + // Merge prompts + const mergedContent = this.mergeContent(matchingDocuments); + const mergedMetadata = this.mergeMetadata(matchingDocuments); + + // Render prompt template + log.trace('[PromptRender] Rendering prompt for mode:', mode, 'with data:', JSON.stringify(data)); + const result = Sqrl.render(mergedContent, data, { varName: 'positron' }) as string; + + return { + content: result, + metadata: mergedMetadata, + }; + } +} + +//#region Prompt management + +async function showInitialPromptPick(renderer: PromptRenderer) { + const context = renderer.extensionContext; + const quickPick = vscode.window.createQuickPick(); + quickPick.placeholder = 'Select a mode'; + + quickPick.items = [ + { label: 'Built-in Modes', kind: vscode.QuickPickItemKind.Separator }, + { label: 'Ask', description: 'Ask mode in the chat panel' }, + { label: 'Edit', description: 'Edit mode in the chat panel' }, + { label: 'Agent', description: 'Agent mode in the chat panel' }, + { label: 'Editor', description: 'Inline editor chat' }, + { label: 'Terminal', description: 'Inline Terminal chat' }, + { label: 'Notebook', description: 'Notebook chat' }, + { label: 'Miscelleaneous', kind: vscode.QuickPickItemKind.Separator }, + { label: 'Reset', description: 'Reset all prompt configuration to the default values.' }, + ]; + + quickPick.onDidAccept(() => { + const selected = quickPick.selectedItems[0]; + quickPick.hide(); + + switch (selected?.label) { + case 'Ask': + showPromptModePick(context, positron.PositronChatMode.Ask); + break; + case 'Edit': + showPromptModePick(context, positron.PositronChatMode.Edit); + break; + case 'Agent': + showPromptModePick(context, positron.PositronChatMode.Agent); + break; + case 'Editor': + showPromptModePick(context, positron.PositronChatAgentLocation.Editor); + break; + case 'Terminal': + showPromptModePick(context, positron.PositronChatAgentLocation.Terminal); + break; + case 'Notebook': + showPromptModePick(context, positron.PositronChatAgentLocation.Notebook); + break; + case 'Reset': + context.globalState.update(PROMPT_MODE_SELECTIONS_KEY, undefined); + break; + } + }); + + quickPick.onDidHide(() => quickPick.dispose()); + quickPick.show(); +} + +async function showPromptModePick(context: vscode.ExtensionContext, mode: PromptMetadataMode) { + const savedSelections = context.globalState?.get(PROMPT_MODE_SELECTIONS_KEY) || {}; + + const quickPick = vscode.window.createQuickPick(); + quickPick.canSelectMany = true; + quickPick.placeholder = 'Select prompts'; + + // Built-in prompts + const docs = PromptRenderer.instance.getModePromptDocuments(mode, false); + const builtinItems = docs.map(doc => { + const label = path.basename(doc.filePath); + const description = doc.metadata.description; + const picked = savedSelections[mode]?.find(s => s.file === label)?.enabled ?? true; + return { label, picked, description }; + }); + + quickPick.items = [ + { label: 'Built-in Prompts', kind: vscode.QuickPickItemKind.Separator }, + ...builtinItems, + ]; + quickPick.selectedItems = quickPick.items.filter(item => item.picked); + + quickPick.onDidAccept(() => { + const selectedItems = quickPick.items + .filter(item => item.kind !== vscode.QuickPickItemKind.Separator) + .map(item => ({ file: item.label, enabled: quickPick.selectedItems.includes(item) })); + + const newSelections = { ...savedSelections, [mode]: selectedItems }; + context.globalState.update(PROMPT_MODE_SELECTIONS_KEY, newSelections); + quickPick.hide(); + }); + + quickPick.onDidHide(() => quickPick.dispose()); + quickPick.show(); +} + +export function registerPromptManagement(context: vscode.ExtensionContext) { + // Intialise prompt renderer + const renderer = new PromptRenderer(context); + + // Register prompt management quickpick command + const disposable = vscode.commands.registerCommand( + 'positron-assistant.managePromptFiles', + () => showInitialPromptPick(renderer) + ); + context.subscriptions.push(disposable); +} diff --git a/extensions/positron-assistant/src/test/participants.test.ts b/extensions/positron-assistant/src/test/participants.test.ts index 322f2c5c09a0..fa1aad6e32a2 100644 --- a/extensions/positron-assistant/src/test/participants.test.ts +++ b/extensions/positron-assistant/src/test/participants.test.ts @@ -12,6 +12,7 @@ import { mock } from './utils.js'; import { readFile } from 'fs/promises'; import { MARKDOWN_DIR } from '../constants.js'; import path = require('path'); +import { PromptRenderer } from '../promptRender.js'; /** We expect 3 messages by default: 1 for the system prompt, 1 for the user's prompt, and 1 containing at least the default context */ const DEFAULT_EXPECTED_MESSAGE_COUNT = 3; @@ -118,6 +119,8 @@ suite('PositronAssistantParticipant', () => { disposables.push(participantService); chatParticipant = new PositronAssistantChatParticipant(extensionContext, participantService); editorParticipant = new PositronAssistantEditorParticipant(extensionContext, participantService); + + new PromptRenderer(extensionContext); }); teardown(() => { @@ -305,36 +308,6 @@ ${attachmentsText} }); }); - test('should include llms.txt instructions', async () => { - // Create an llms.txt file in the workspace. - const llmsTxtContent = `This is a test llms.txt file. -It should be included in the chat message.`; - await vscode.workspace.fs.writeFile(llmsTxtUri, Buffer.from(llmsTxtContent)); - - try { - // Setup test inputs. - const request = makeChatRequest({ model, references: [] }); - const context: vscode.ChatContext = { history: [] }; - sinon.stub(positron.ai, 'getPositronChatContext').resolves({}); - const sendRequestSpy = sinon.spy(model, 'sendRequest'); - - // Call the method under test. - await chatParticipant.requestHandler(request, context, response, token); - - // The first user message should contain the formatted context. - sinon.assert.calledOnce(sendRequestSpy); - const [messages,] = sendRequestSpy.getCall(0).args; - assert.strictEqual(messages.length, DEFAULT_EXPECTED_MESSAGE_COUNT, `Unexpected messages: ${JSON.stringify(messages)}`); - assertContextMessage(messages.at(-2)!, - ` -${llmsTxtContent} -`); - } finally { - // Delete the llms.txt file from the workspace. - await vscode.workspace.fs.delete(llmsTxtUri); - } - }); - test('should include editor information', async () => { const document = await vscode.workspace.openTextDocument(fileReferenceUri); const selection = new vscode.Selection(new vscode.Position(0, 0), new vscode.Position(1, 0)); diff --git a/extensions/positron-assistant/src/tools/projectTreeTool.ts b/extensions/positron-assistant/src/tools/projectTreeTool.ts index e2ce207446e5..1a0369746d84 100644 --- a/extensions/positron-assistant/src/tools/projectTreeTool.ts +++ b/extensions/positron-assistant/src/tools/projectTreeTool.ts @@ -8,13 +8,13 @@ import { PositronAssistantToolName } from '../types.js'; import { log } from '../extension.js'; /** - * Represents either a file (string) or a directory (tuple with string and children). + * Represents either a file or a directory */ -type DirectoryItem = string | [string, DirectoryItem[]]; +type DirectoryItem = string; type DirectoryInfo = { folder: vscode.WorkspaceFolder; - items: DirectoryItem; + items: DirectoryItem[]; totalFiles: number; }; @@ -99,133 +99,29 @@ export const ProjectTreeTool = vscode.lm.registerTool(Positron findOptions, token ); - const items = convertUrisToDirectoryItems(folder, matchedFileUris); + const items = matchedFileUris.map(uri => vscode.workspace.asRelativePath(uri, false)); workspaceTrees.push({ folder, items, totalFiles: matchedFileUris.length }); } const totalFiles = workspaceTrees.reduce((sum, obj) => sum + obj.totalFiles, 0); log.debug(`[${PositronAssistantToolName.ProjectTree}] Project tree constructed with ${totalFiles} items across ${workspaceFolders.length} workspace folders.`); - - // Return a compressed description of the project tree if there are too many items if (totalFiles > filesLimit) { - const itemLimit = Math.floor(filesLimit / workspaceTrees.length); log.debug(`[${PositronAssistantToolName.ProjectTree}] Project tree exceeds the limit of ${filesLimit} items. A summary will be returned for each workspace folder.`); - const summarizedTree = await getSummarizedProjectTree(workspaceTrees, itemLimit); - return new vscode.LanguageModelToolResult([ - new vscode.LanguageModelTextPart(`Project tree contains ${totalFiles} files, which exceeds the limit of ${filesLimit}. Here is a summary of each workspace, including the first ${itemLimit} files and directories:`), - new vscode.LanguageModelTextPart(JSON.stringify(summarizedTree)), - ]); } - // Return the project tree as a JSON string to the model - return new vscode.LanguageModelToolResult( - workspaceTrees.map(obj => new vscode.LanguageModelTextPart(JSON.stringify(obj))) - ); + // Return a compressed description of the project tree if there are too many items + const itemLimit = Math.floor(filesLimit / workspaceTrees.length); + const results = workspaceTrees.map(obj => obj.items + .sort((a, b) => a.length - b.length) // Shortest paths first + .slice(0, itemLimit) // Remove deepest paths to fit within the limit + .sort((a, b) => a.localeCompare(b)) // Resort alphabetically + .join('\n')); + + return new vscode.LanguageModelToolResult(results.map(r => new vscode.LanguageModelTextPart(r))); } }); -/** - * Constructs a summarized project tree from the workspace trees. - * @param workspaceTrees An array of DirectoryInfo objects representing the workspace folders and their contents. - * @param itemLimit The maximum number of items to include in the summary for each workspace. - * This is split evenly between files and directories. - * For example, if itemLimit is 10, it will include 5 files and 5 directories. - * @returns A summarized project tree object. - */ -async function getSummarizedProjectTree(workspaceTrees: DirectoryInfo[], itemLimit: number) { - if (workspaceTrees.length === 0) { - return {}; - } - - const summary = new Map(); - for (const workspace of workspaceTrees) { - summary.set(workspace.folder.name, { - totalFiles: workspace.totalFiles, - files: [], - directories: [], - workspaceUri: workspace.folder.uri.toString(), - }); - - // Get the top-level items in the workspace folder - const items = (await vscode.workspace.fs.readDirectory(workspace.folder.uri)); - if (items.length === 0) { - continue; - } - - // Separate files and directories - const files: string[] = []; - const directories: string[] = []; - for (const [name, type] of items) { - if (type === vscode.FileType.File) { - files.push(name); - } else if (type === vscode.FileType.Directory) { - directories.push(name); - } - } - - // Sort files and directories alphabetically - files.sort(); - directories.sort(); - - // Use half the limit for files and half for directories - const fileLimit = Math.floor(itemLimit / 2); - const dirLimit = itemLimit - fileLimit; - - // Slice the files and directories to fit within the limits - summary.get(workspace.folder.name)!.files = files.slice(0, fileLimit); - summary.get(workspace.folder.name)!.directories = directories.slice(0, dirLimit); - } - - return Object.fromEntries(summary); -} - -/** - * Convert an array of URIs to a directory structure for a workspace folder. - * @param folder The workspace folder to which the URIs belong. - * @param uris The URIs of items in the project tree. - * @returns DirectoryItem representing the directory structure of the workspace folder. - */ -function convertUrisToDirectoryItems(folder: vscode.WorkspaceFolder, uris: vscode.Uri[]): DirectoryItem { - // If there are no URIs, return an empty directory structure for the folder - if (uris.length === 0) { - return [folder.name, []]; - } - - // Sort the URIs alphabetically (folders and files are sorted together) - uris.sort((a, b) => a.fsPath.localeCompare(b.fsPath)); - - const root: Record = {}; - for (const uri of uris) { - // Get the path relative to the workspace folder, e.g. src/myfolder/myfile.txt - const relativePath = vscode.workspace.asRelativePath(uri, false); - - // Split the relative path into segments, e.g. ['src', 'myfolder', 'myfile.txt'] - const segments = relativePath.split('/'); - let node = root; - - // Iterate through the segments to build the directory structure - for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - const isLastSegment = i === segments.length - 1; - if (isLastSegment) { - node[segment] = null; - } else { - node[segment] = node[segment] || {}; - node = node[segment]; - } - } - } - - // Convert the nested object structure to an array of DirectoryItems - const toDirectoryItems = (node: Record): DirectoryItem[] => - Object.entries(node).map(([name, value]) => - value === null ? name : [name, toDirectoryItems(value)] - ); - - return [folder.name, toDirectoryItems(root)]; -} - /** * Convert from the tool input excludeSettings to the vscode.ExcludeSettingOptions * @param excludeSetting The exclude setting from the tool input. diff --git a/extensions/positron-assistant/src/types.ts b/extensions/positron-assistant/src/types.ts index 36192b802308..298cb73b1e54 100644 --- a/extensions/positron-assistant/src/types.ts +++ b/extensions/positron-assistant/src/types.ts @@ -100,6 +100,16 @@ export interface RuntimeSessionReference { variables: Variable[]; } +/** + * A prompt instructions file reference. + */ +export interface PromptInstructionsReference { + id: string; + modelDescription: string; + name: string; + value: vscode.Uri; +} + /** * A single variable in the runtime. * diff --git a/extensions/positron-assistant/src/utils.ts b/extensions/positron-assistant/src/utils.ts index 026bdf25f03e..1160c652f759 100644 --- a/extensions/positron-assistant/src/utils.ts +++ b/extensions/positron-assistant/src/utils.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import * as ai from 'ai'; import { JSONTree } from '@vscode/prompt-tsx'; -import { LanguageModelCacheBreakpoint, LanguageModelCacheBreakpointType, LanguageModelDataPartMimeType, PositronAssistantToolName, RuntimeSessionReference } from './types.js'; +import { LanguageModelCacheBreakpoint, LanguageModelCacheBreakpointType, LanguageModelDataPartMimeType, PositronAssistantToolName, PromptInstructionsReference, RuntimeSessionReference } from './types.js'; import { log } from './extension.js'; /** @@ -630,3 +630,15 @@ export function isRuntimeSessionReference(value: unknown): value is RuntimeSessi 'variables' in value && Array.isArray(value.variables); } + +/** + * Type guard to check if a reference is a prompt instructions file + */ +export function isPromptInstructionsReference(reference: unknown): reference is PromptInstructionsReference { + return typeof reference === 'object' && reference !== null && + 'modelDescription' in reference && + 'name' in reference && + 'id' in reference && typeof reference.id === 'string' && + 'value' in reference && reference.value instanceof vscode.Uri && + reference.id.includes('vscode.prompt.instructions'); +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index d0c59a266253..bf1d2a278184 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -49,7 +49,7 @@ import { ILanguageModelsService, LanguageModelsService } from '../common/languag import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; -import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, MODE_DEFAULT_SOURCE_FOLDER, MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../common/promptSyntax/config/promptFileLocations.js'; +import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTIONS_POSITRON_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, MODE_DEFAULT_SOURCE_FOLDER, MODE_FILE_EXTENSION, MODE_POSITRON_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, PROMPT_POSITRON_SOURCE_FOLDER } from '../common/promptSyntax/config/promptFileLocations.js'; import { registerPromptFileContributions } from '../common/promptSyntax/promptFileContributions.js'; import { INSTRUCTIONS_DOCUMENTATION_URL, MODE_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/promptTypes.js'; import { IPromptsService } from '../common/promptSyntax/service/promptsService.js'; @@ -541,6 +541,9 @@ configurationRegistry.registerConfiguration({ ), default: { [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, + // --- Start Positron --- + [INSTRUCTIONS_POSITRON_SOURCE_FOLDER]: true, + // --- End Positron --- }, additionalProperties: { type: 'boolean' }, restricted: true, @@ -569,6 +572,9 @@ configurationRegistry.registerConfiguration({ ), default: { [PROMPT_DEFAULT_SOURCE_FOLDER]: true, + // --- Start Positron --- + [PROMPT_POSITRON_SOURCE_FOLDER]: true, + // --- End Positron --- }, additionalProperties: { type: 'boolean' }, unevaluatedProperties: { type: 'boolean' }, @@ -598,6 +604,9 @@ configurationRegistry.registerConfiguration({ ), default: { [MODE_DEFAULT_SOURCE_FOLDER]: true, + // --- Start Positron --- + [MODE_POSITRON_SOURCE_FOLDER]: true, + // --- End Positron --- }, additionalProperties: { type: 'boolean' }, unevaluatedProperties: { type: 'boolean' }, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index cef2626639f7..1d7bd39d17ff 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -202,12 +202,30 @@ export class ComputeAutomaticInstructions { const resolvedRoots = await this._fileService.resolveAll(folders.map(f => ({ resource: f.uri }))); for (const root of resolvedRoots) { if (root.success && root.stat?.children) { + // --- Start Positron --- + // Also check for additional agent instructions files + /* const agentMd = root.stat.children.find(c => c.isFile && c.name.toLowerCase() === 'agents.md'); if (agentMd) { entries.add(toPromptFileVariableEntry(agentMd.resource, PromptFileVariableKind.Instruction, localize('instruction.file.reason.agentsmd', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_AGENT_MD), true)); telemetryEvent.agentInstructionsCount++; this._logService.trace(`[InstructionsContextComputer] AGENTS.md files added: ${agentMd.resource.toString()}`); } + */ + const agentMds = root.stat.children.filter( + c => c.isFile && !c.isSymbolicLink && c.name.toLowerCase() === 'agents.md' || + c.name.toLowerCase() === 'agent.md' || + c.name.toLowerCase() === 'positron.md' || + c.name.toLowerCase() === 'claude.md' || + c.name.toLowerCase() === 'gemini.md' || + c.name.toLowerCase() === 'llms.txt' + ); + for (const md of agentMds) { + entries.add(toPromptFileVariableEntry(md.resource, PromptFileVariableKind.Instruction, localize('instruction.file.reason.agentsmd', 'Automatically attached as setting {0} is enabled', PromptsConfig.USE_AGENT_MD), true)); + telemetryEvent.agentInstructionsCount++; + this._logService.trace(`[InstructionsContextComputer] AGENTS.md files added: ${md.resource.toString()}`); + } + // --- End Positron --- } } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts index 007495ef8531..a588c2ec6612 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts @@ -43,6 +43,13 @@ export const INSTRUCTIONS_DEFAULT_SOURCE_FOLDER = '.github/instructions'; */ export const MODE_DEFAULT_SOURCE_FOLDER = '.github/chatmodes'; +// --- Start Positron --- +// Additional search paths for `.positron` directory +export const PROMPT_POSITRON_SOURCE_FOLDER = '.positron/prompts'; +export const INSTRUCTIONS_POSITRON_SOURCE_FOLDER = '.positron/instructions'; +export const MODE_POSITRON_SOURCE_FOLDER = '.positron/chatmodes'; +// --- End Positron --- + /** * Gets the prompt file type from the provided path. */