Compare commits

35 Commits

Author SHA1 Message Date
a89b6d757e Update .vscodeignore to exclude additional asset directories and files 2025-03-28 04:27:09 +00:00
1924971d22 Add avatar image asset 2025-03-28 04:27:05 +00:00
a31a1bd3e2 fix upload path
All checks were successful
Release VSCode Extension / release (push) Successful in 33s
2025-03-27 10:04:39 +00:00
d6450fd597 Add ajv
Some checks failed
Release VSCode Extension / release (push) Failing after 43s
2025-03-27 10:02:27 +00:00
c4f97e252d Bump node 2025-03-27 10:01:46 +00:00
77e4df420e Deps sorting and codegen for build
Some checks failed
Release VSCode Extension / release (push) Failing after 40s
2025-03-27 07:23:40 +00:00
f6467a82c2 refactor comments 2025-03-27 07:17:22 +00:00
d895f58963 no global vsce and using new name
Some checks failed
Release VSCode Extension / release (push) Failing after 26s
2025-03-27 07:14:53 +00:00
d9e44b78ce Merge pull request 'sorting-files' (#2) from sorting-files into main
Some checks failed
Release VSCode Extension / release (push) Failing after 1m22s
Reviewed-on: #2
2025-03-27 07:10:17 +00:00
4b38333d23 Release workflow 2025-03-27 07:08:29 +00:00
bea2ea2a0f Sort folders first then files 2025-03-27 06:28:16 +00:00
8246879bea Fix missing newline at end of file in extension.ts 2025-03-17 07:24:19 +00:00
72f21325dd Merge pull request 'feat-filter-ignore' (#1) from feat-filter-ignore into main
Reviewed-on: #1
2025-03-17 07:23:37 +00:00
7aece43a6b Enhance status bar with new buttons for tree view and settings; refactor existing button setup 2025-03-17 07:22:40 +00:00
d2b09eefbe Revise README.md for clarity and conciseness, updating sections on usage, features, and installation instructions 2025-03-13 08:52:22 +00:00
19a5dc658e Fix formatting instructions in XML documentation for clarity and correct spelling 2025-03-13 08:49:46 +00:00
f0f5b315e7 Update CHANGELOG.md to include initial release details and added features 2025-03-13 08:41:46 +00:00
5bb5236d3a Remove unused plain text prompt generation method from PromptGenerator 2025-03-13 08:29:43 +00:00
7eb0d49d07 Refactor test setup and improve prompt generation error handling 2025-03-13 08:28:00 +00:00
69475782eb Enhance file selection by loading ignore patterns from .gitignore in parent directories 2025-03-13 08:12:06 +00:00
385e35a8ad Use webpack bundler 2025-03-13 08:00:45 +00:00
2df0dc666b Intermediate still contains errors, but an attempt to solve filtering as per .gitignore 2025-03-12 15:05:52 +00:00
c18e4dac10 Merge pull request #1 from abhishekbhakat/feat-settings
Feat settings
2025-03-12 09:09:19 +00:00
5a6e1a5e90 remove duplication 2025-03-12 09:08:56 +00:00
134bdf1e5e Assistant checkpoint: Fixed tree provider implementation and recompiled
Assistant generated file changes:
- src/providers/prompterTreeProvider.ts: Fix imports and improve modularity

---

User prompt:

This broke the import of ignore and caused `There is no data provider registered that can provide view data.`

Replit-Commit-Author: Assistant
Replit-Commit-Session-Id: e2f69f78-99c3-447b-a97a-9a2c4347a1d6
2025-03-12 08:39:44 +00:00
5a4560758c ignore config 2025-03-12 08:06:19 +00:00
926c88780b Assistant checkpoint: Refactored prompterTreeProvider.ts into modular components
Assistant generated file changes:
- src/providers/prompterTreeProvider.ts: Refactor to a more modular structure
- src/providers/fileSelectionManager.ts: Create file selection manager class
- src/providers/settingsManager.ts: Create settings manager class

---

User prompt:

Can you help me make this file more modular?
@src/providers/prompterTreeProvider.ts

Replit-Commit-Author: Assistant
Replit-Commit-Session-Id: e2f69f78-99c3-447b-a97a-9a2c4347a1d6
2025-03-12 08:01:18 +00:00
2d28f71e9b add support for recursive selection and deselection of files in the tree view 2025-03-12 07:25:26 +00:00
65bf168e1f add conditional visibility for prompter commands and include 'ignore' dependency 2025-03-12 06:54:12 +00:00
6e3fbd8f2b add repository URL to package.json 2025-03-12 06:44:37 +00:00
3adfd712c0 add XML formatting instructions file and update prompt generator to include instructions 2025-03-12 06:42:53 +00:00
759cfad76e ignore assets 2025-03-12 05:23:36 +00:00
0ec4e8e2d2 follow xml pattern 2025-03-11 18:40:20 +00:00
3676136692 modular code 2025-03-11 18:27:57 +00:00
6bd50154f0 add settings management and UI integration 2025-03-11 18:18:32 +00:00
23 changed files with 1602 additions and 351 deletions

View File

@@ -0,0 +1,84 @@
name: Release VSCode Extension
run-name: ${{ gitea.actor }} is releasing a new version 🚀
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
release:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository code
- name: Checkout code
uses: actions/checkout@v4
# Step 2: Set up Node.js environment
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '23.9.0'
# Step 3: Install jq for JSON parsing
- name: Install jq
run: sudo apt-get install -y jq
# Step 4: Install project dependencies inside prompter directory
- name: Install dependencies
run: npm install
# Step 5: Install vsce
- name: Install vsce
run: npm install @vscode/vsce
# Step 6: Install additional dev dependencies for building inside prompter directory
- name: Install build tools
run: npm install --save-dev webpack webpack-cli ts-loader codegen
# Step 7: Build the extension inside prompter directory
- name: Build the extension
run: npm run webpack:prod
# Step 8: Package the extension into a VSIX file inside prompter directory
- name: Package the extension
run: npx vsce package
# Step 9: Extract the tag name from gitea.ref
- name: Extract tag name
run: |
TAG_NAME=$(echo "${{ gitea.ref }}" | sed 's/refs\/tags\///')
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
# Step 10: Determine the VSIX file name from package.json inside prompter directory
- name: Get VSIX file name
run: |
NAME=$(jq -r .name package.json)
VERSION=$(jq -r .version package.json)
VSIX_FILE="${NAME}-${VERSION}.vsix"
echo "VSIX_FILE=$VSIX_FILE" >> $GITHUB_ENV
# Step 11: Create a new release in Gitea
- name: Create release
run: |
curl -X POST \
-H "Authorization: token ${{ gitea.token }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG_NAME\", \"name\": \"Release $TAG_NAME\", \"body\": \"Automated release for version $TAG_NAME\"}" \
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases
# Step 12: Retrieve the release ID
- name: Get release ID
run: |
RELEASE_ID=$(curl -s -H "Authorization: token ${{ gitea.token }}" \
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases/tags/$TAG_NAME | jq .id)
echo "RELEASE_ID=$RELEASE_ID" >> $GITHUB_ENV
# Step 13: Upload the VSIX file as a release asset (from prompter directory)
- name: Upload VSIX to release
run: |
curl -X POST \
-H "Authorization: token ${{ gitea.token }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$VSIX_FILE" \
https://git.bhakat.dev/api/v1/repos/abhishekbhakat/Prompter/releases/$RELEASE_ID/assets?name=$VSIX_FILE

2
.gitignore vendored
View File

@@ -137,3 +137,5 @@ dist
package-lock.json
*.vsix
attached_assets

12
.vscode-test.mjs Normal file
View File

@@ -0,0 +1,12 @@
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default {
version: 'stable',
extensionDevelopmentPath: __dirname,
extensionTestsPath: path.join(__dirname, 'out', 'test'),
testFiles: ['**/**.test.js'],
workspaceFolder: __dirname
};

View File

@@ -8,6 +8,8 @@ src/**
**/.eslintrc.json
**/*.map
**/*.ts
out/**
webpack.config.js
.replit
.breakpoints
.local/**
@@ -25,4 +27,9 @@ generated-icon.png
out/test/**
package-lock.json
# Assets not needed in the final package
attached_assets/**
attached_assets/**
.config
assets/
.gitea/**
*.md
.vscode-test.mjs

View File

@@ -6,4 +6,10 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
## [Unreleased]
- Initial release
- Initial release
## [0.0.1] - 2025-03-13
### Added
- PromptGeneration extension with basic file selection and xml edits option.

161
README.md
View File

@@ -1,153 +1,48 @@
# Prompter
Code smarter with AI—no more messy copy-pasting. Prompter structures your prompts and applies AI changes seamlessly, streamlining your coding workflow.
Enhance your coding with AI—Prompter organizes your prompts and implements AI changes effortlessly, improving your development workflow.
## Why Prompter?
## Why Use Prompter?
- **Too much bloat in your repo?** Stop zipping everything—send only the files that matter.
- **LLM underperforming?** Cut the noise for sharper, more accurate responses.
- **Better AI coding?** Select just the right context to optimize results.
- **Reduce repository clutter** - Share only essential files instead of everything
- **Improve AI responses** - Eliminate irrelevant context for better results
- **Optimize coding assistance** - Target exactly what you need for precise AI help
Prompter empowers you to work efficiently with AI, reducing token waste and improving clarity.
Prompter helps you collaborate efficiently with AI, minimizing token usage and enhancing clarity.
## Features
## Key Features
- **Advanced File Selection & Token Estimation**
Precisely filter files and estimate token usage instantly for optimized, cost-effective prompts.
- **Smart File Selection & Token Counting**
Filter files and track token usage for efficient, economical prompts
- **Optimized XML Prompt**
Structured file trees, CodeMaps, content, and instructions in XML for maximum LLM clarity.
- **XML-Based Prompt Structure**
Organize file trees, CodeMaps, and instructions in XML for better AI comprehension
- **Structured XML Diffs**
Converts LLM-generated XML edits into precise, reviewable diffs—works at any file size.
- **Clean XML Diff Application**
Transform AI-generated XML changes into clear, reviewable diffs at any scale
- **Codemap Extraction**
Scans files locally to extract classes, functions, and references, minimizing tokens and hallucinations. Auto-detects referenced types.
- **Intelligent Code Scanning**
Extract code structure locally to reduce tokens and prevent hallucinations
- **Mac-Native Performance**
Built for macOS with native speed and responsiveness—because performance matters.
- **Cross-Platform Support**
Designed for any VSCode platform with native performance and responsiveness
- **Clipboard Integration**
Copy structured prompts into any AI chat app—your data stays local, no external API needed.
- **Direct Clipboard Support**
Copy structured prompts to any AI platform with your data remaining local
- **Works with Any Model**
Compatible with OpenAI, Anthropic, DeepSeek, Gemini, Azure, OpenRouter, and local models—private and offline when you need it.
- **Privacy-Focused Design**
Process everything locally without sending data to third parties
- **Privacy First**
Local models, offline scanning, and direct clipboard use—no intermediaries required.
## Quick Start
## Installation
*(Note: Installation steps are assumed based on the VS Code context from other files. Adjust as needed.)*
1. Clone the repository:
```bash
git clone <repository-url>
```
2. Open the project in VS Code.
3. Install dependencies:
```bash
npm install
```
4. Build the extension:
```bash
npm run compile
```
5. Press `F5` in VS Code to launch the extension in a development window.
## Usage
1. Open your project in VS Code.
2. Use the Prompter interface to select files and estimate tokens.
3. Generate a structured XML prompt via the clipboard.
4. Paste into your preferred AI model (e.g., ChatGPT, Claude, or a local LLM).
5. Apply the returned XML diffs directly through Prompter for seamless integration.
1. Install the extension in VS Code
2. Select relevant files through the Prompter sidebar
3. Generate and copy a structured XML prompt
4. Paste into your AI tool of choice
## Contributing
We welcome contributions! To get started:
1. Fork the repository.
2. Create a feature branch: `git checkout -b my-feature`.
3. Commit your changes: `git commit -m "Add my feature"`.
4. Push to your branch: `git push origin my-feature`.
5. Open a pull request.
See `vsc-extension-quickstart.md` for development setup and testing details.
Contributions welcome!
---
Built with ❤️ by the Prompter team.
Code smarter with AI—no more messy copy-pasting. Prompter structures your prompts and applies AI changes seamlessly, streamlining your coding workflow.
## Why Prompter?
- **Too much bloat in your repo?** Stop zipping everything—send only the files that matter.
- **LLM underperforming?** Cut the noise for sharper, more accurate responses.
- **Better AI coding?** Select just the right context to optimize results.
Prompter empowers you to work efficiently with AI, reducing token waste and improving clarity.
## Features
- **Advanced File Selection & Token Estimation**
Precisely filter files and estimate token usage instantly for optimized, cost-effective prompts.
- **Optimized XML Prompt**
Structured file trees, CodeMaps, content, and instructions in XML for maximum LLM clarity.
- **Structured XML Diffs**
Converts LLM-generated XML edits into precise, reviewable diffs—works at any file size.
- **Codemap Extraction**
Scans files locally to extract classes, functions, and references, minimizing tokens and hallucinations. Auto-detects referenced types.
- **Mac-Native Performance**
Built for macOS with native speed and responsiveness—because performance matters.
- **Clipboard Integration**
Copy structured prompts into any AI chat app—your data stays local, no external API needed.
- **Works with Any Model**
Compatible with OpenAI, Anthropic, DeepSeek, Gemini, Azure, OpenRouter, and local models—private and offline when you need it.
- **Privacy First**
Local models, offline scanning, and direct clipboard use—no intermediaries required.
## Installation
*(Note: Installation steps are assumed based on the VS Code context from other files. Adjust as needed.)*
1. Clone the repository:
```bash
git clone <repository-url>
```
2. Open the project in VS Code.
3. Install dependencies:
```bash
npm install
```
4. Build the extension:
```bash
npm run compile
```
5. Press `F5` in VS Code to launch the extension in a development window.
## Usage
1. Open your project in VS Code.
2. Use the Prompter interface to select files and estimate tokens.
3. Generate a structured XML prompt via the clipboard.
4. Paste into your preferred AI model (e.g., ChatGPT, Claude, or a local LLM).
5. Apply the returned XML diffs directly through Prompter for seamless integration.
## Contributing
We welcome contributions! To get started:
1. Fork the repository.
2. Create a feature branch: `git checkout -b my-feature`.
3. Commit your changes: `git commit -m "Add my feature"`.
4. Push to your branch: `git push origin my-feature`.
5. Open a pull request.
See `vsc-extension-quickstart.md` for development setup and testing details.
---
Built with ❤️ by the Prompter team.
Built with ❤️ by the Prompter team.

BIN
assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 KiB

19
jest.config.js Normal file
View File

@@ -0,0 +1,19 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\.tsx?$': ['ts-jest', {}],
},
testMatch: ['**/test/**/*.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json',
},
},
// Mock the vscode module since it's not available during testing
moduleNameMapper: {
'^vscode$': '<rootDir>/src/test/vscode-mock.ts'
},
};

View File

@@ -2,8 +2,9 @@
"name": "prompter",
"displayName": "Prompter",
"description": "Easy prompt generation and apply edits using prompter.",
"version": "0.0.1",
"version": "0.0.2",
"publisher": "abhishekbhakat",
"repository": "https://git.bhakat.dev/abhishekbhakat/Prompter",
"engines": {
"vscode": "^1.98.0"
},
@@ -11,7 +12,7 @@
"Other"
],
"activationEvents": [],
"main": "./out/extension.js",
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
@@ -20,7 +21,18 @@
},
{
"command": "prompter.generatePrompt",
"title": "Copy"
"title": "Copy",
"icon": "$(clippy)"
},
{
"command": "prompter.openTreeView",
"title": "Show Tree View",
"icon": "$(list-tree)"
},
{
"command": "prompter.openSettings",
"title": "Settings",
"icon": "$(settings-gear)"
}
],
"viewsContainers": {
@@ -36,7 +48,8 @@
"prompter-sidebar": [
{
"id": "prompterView",
"name": "Prompter"
"name": "Prompter",
"icon": "resources/icon.svg"
}
]
},
@@ -44,7 +57,18 @@
"view/title": [
{
"command": "prompter.generatePrompt",
"group": "navigation"
"group": "navigation@1",
"when": "view == prompterView"
},
{
"command": "prompter.openTreeView",
"group": "navigation@2",
"when": "view == prompterView"
},
{
"command": "prompter.openSettings",
"group": "navigation@3",
"when": "view == prompterView"
}
],
"view/item/context": [
@@ -56,12 +80,14 @@
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"vscode:prepublish": "npm run webpack:prod",
"webpack:dev": "webpack --mode development",
"webpack:prod": "webpack --mode production",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"watch": "webpack --watch --mode development",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test"
"test": "node ./src/test/runTest.mjs"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
@@ -71,10 +97,14 @@
"@typescript-eslint/parser": "^8.25.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"ajv": "^8.17.0",
"eslint": "^9.21.0",
"typescript": "^5.7.3"
"ts-loader": "^9.5.2",
"typescript": "^5.7.3",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1"
},
"dependencies": {
"@vscode/vsce": "^3.2.2"
"ignore": "^7.0.3"
}
}

View File

@@ -0,0 +1,294 @@
<xml_formatting_instructions>
### Role
- You are a **code editing assistant**: You can fulfill edit requests and chat with the user about code or other questions. Provide complete instructions or code lines when replying with xml formatting.
### Capabilities
- Can create new files.
- Can rewrite entire files.
- Can perform partial search/replace modifications.
- Can delete existing files.
Avoid placeholders like `...` or `// existing code here`. Provide complete lines or code.
## Tools & Actions
1. **create** Create a new file if it doesnt exist.
2. **rewrite** Replace the entire content of an existing file.
3. **modify** (search/replace) For partial edits with <search> + <content>.
4. **delete** Remove a file entirely (empty <content>).
### **Format to Follow for Diff Protocol**
<Plan>
Describe your approach or reasoning here.
</Plan>
<file path="path/to/example.swift" action="one_of_the_tools">
<change>
<description>Brief explanation of this specific change</description>
<search>
===
// Exactly matching lines to find
===
</search>
<content>
===
// Provide the new or updated code here. Do not use placeholders
===
</content>
</change>
<!-- Add more <change> blocks if you have multiple edits for the same file -->
</file>
#### Tools Demonstration
1. `<file path="NewFile.swift" action="create">` Full file in <content>
2. `<file path="DeleteMe.swift" action="delete">` Empty <content>
3. `<file path="ModifyMe.swift" action="modify">` Partial edit with `<search>` + `<content>`
4. `<file path="RewriteMe.swift" action="rewrite">` Entire file in <content>
5. `<file path="RewriteMe.swift" action="rewrite">` Entire file in <content>. No <search> required.
## Format Guidelines
1. **Plan**: Begin with a `<Plan>` block explaining your approach.
2. **<file> Tag**: e.g. `<file path="Models/User.swift" action="...">`. Must match an available tool.
3. **<change> Tag**: Provide `<description>` to clarify each change. Then `<content>` for new/modified code. Additional rules depend on your capabilities.
4. **modify**: **<search> & <content>**: Provide code blocks enclosed by ===. Respect indentation exactly, ensuring the <search> block matches the original source down to braces, spacing, and any comments. The new <content> will replace the <search> block, and should should fit perfectly in the space left by it's removal.
5. **modify**: For changes to the same file, ensure that you use multiple change blocks, rather than separate file blocks.
6. **rewrite**: For large overhauls; omit `<search>` and put the entire file in `<content>`.
7. **create**: For new files, put the full file in <content>.
8. **delete**: Provide an empty <content>. The file is removed.
## Code Examples
-----
### Example: Search and Replace (Add email property)
<Plan>
Add an email property to `User` via search/replace.
</Plan>
<file path="Models/User.swift" action="modify">
<change>
<description>Add email property to User struct</description>
<search>
===
struct User {
let id: UUID
var name: String
}
===
</search>
<content>
===
struct User {
let id: UUID
var name: String
var email: String
}
===
</content>
</change>
</file>
-----
### Example: Negative Example - Mismatched Search Block
// Example Input (not part of final output, just demonstration)
<file_contents>
File: path/service.swift
```
import Foundation
class Example {
foo() {
Bar()
}
}
```
</file_contents>
<Plan>
Demonstrate how a mismatched search block leads to failed merges.
</Plan>
<file path="path/service.swift" action="modify">
<change>
<description>This search block is missing or has mismatched indentation, braces, etc.</description>
<search>
===
foo() {
Bar()
}
===
</search>
<content>
===
foo() {
Bar()
Bar2()
}
===
</content>
</change>
</file>
<!-- This example fails because the <search> block doesn't exactly match the original file contents. -->
-----
### Example: Negative Example - Mismatched Brace Balance
// This negative example shows how adding extra braces in the <content> can break brace matching.
<Plan>
Demonstrate that the new content block has one extra closing brace, causing mismatched braces.
</Plan>
<file path="Functions/MismatchedBracesExample.swift" action="modify">
<change>
<description>Mismatched brace balance in the replacement content</description>
<search>
===
foo() {
Bar()
}
===
</search>
<content>
===
foo() {
Bar()
}
bar() {
foo2()
}
}
===
</content>
</change>
</file>
<!-- Because the <search> block was only a small brace segment, adding extra braces in <content> breaks the balance. -->
-----
### Example: Negative Example - One-Line Search Block
<Plan>
Demonstrate a one-line search block, which is too short to be reliable.
</Plan>
<file path="path/service.swift" action="modify">
<change>
<description>One-line search block is ambiguous</description>
<search>
===
var email: String
===
</search>
<content>
===
var emailNew: String
===
</content>
</change>
</file>
<!-- This example fails because the <search> block is only one line and ambiguous. -->
-----
### Example: Negative Example - Ambiguous Search Block
<Plan>
Demonstrate an ambiguous search block that can match multiple blocks (e.g., multiple closing braces).
</Plan>
<file path="path/service.swift" action="modify">
<change>
<description>Ambiguous search block with multiple closing braces</description>
<search>
===
}
}
===
</search>
<content>
===
foo() {
}
}
}
===
</content>
</change>
</file>
<!-- This example fails because the <search> block is ambiguous due to multiple matching closing braces. -->
-----
### Example: Full File Rewrite
<Plan>
Rewrite the entire User file to include an email property.
</Plan>
<file path="Models/User.swift" action="rewrite">
<change>
<description>Full file rewrite with new email field</description>
<content>
===
import Foundation
struct User {
let id: UUID
var name: String
var email: String
init(name: String, email: String) {
self.id = UUID()
self.name = name
self.email = email
}
}
===
</content>
</change>
</file>
-----
### Example: Create New File
<Plan>
Create a new RoundedButton for a custom Swift UIButton subclass.
</Plan>
<file path="Views/RoundedButton.swift" action="create">
<change>
<description>Create custom RoundedButton class</description>
<content>
===
import UIKit
@IBDesignable
class RoundedButton: UIButton {
@IBInspectable var cornerRadius: CGFloat = 0
}
===
</content>
</change>
</file>
-----
### Example: Delete a File
<Plan>
Remove an obsolete file.
</Plan>
<file path="Obsolete/File.swift" action="delete">
<change>
<description>Completely remove the file from the project</description>
<content>
===
===
</content>
</change>
</file>
## Final Notes
1. **modify** Always wrap the exact original lines in <search> and your updated lines in <content>, each enclosed by ===.
2. **modify** The <search> block must match the source code exactly—down to indentation, braces, spacing, and any comments. Even a minor mismatch causes failed merges.
3. **modify** Only replace exactly what you need. Avoid including entire functions or files if only a small snippet changes, and ensure the <search> content is unique and easy to identify.
4. **rewrite** Use `rewrite` for major overhauls, and `modify` for smaller, localized edits. Rewrite requires the entire code to be replaced, so use it sparingly.
5. You can always **create** new files and **delete** existing files. Provide full code for create, and empty content for delete. Avoid creating files you know exist already.
6. If a file tree is provided, place your files logically within that structure. Respect the users relative or absolute paths.
7. Wrap your final output in ```XML ... ``` for clarity.
8. **Important:** Do not wrap any XML output in CDATA tags (i.e. `<![CDATA[ ... ]]>`). We xpect raw XML exactly as shown in the examples.
9. **IMPORTANT** IF MAKING FILE CHANGES, YOU MUST USE THE AVAILABLE XML FORMATTING CAPABILITIES PROVIDED ABOVE - IT IS THE ONLY WAY FOR YOUR CHANGES TO BE APPLIED.
10. The final output must apply cleanly with no leftover syntax errors.
</xml_formatting_instructions>

View File

@@ -1,210 +1,98 @@
// The module 'vscode' contains the VS Code extensibility API
import * as vscode from 'vscode';
import * as path from 'path';
// Custom TreeItem for files and folders
class FileTreeItem extends vscode.TreeItem {
constructor(
public readonly resourceUri: vscode.Uri,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly isDirectory: boolean
) {
super(resourceUri, collapsibleState);
this.contextValue = isDirectory ? 'directory' : 'file';
this.tooltip = resourceUri.fsPath;
this.description = isDirectory ? 'folder' : path.extname(resourceUri.fsPath);
this.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
}
}
// Import our modular components
import { PrompterSettings, DEFAULT_SETTINGS } from './models/settings';
import { FileTreeItem, SettingTreeItem } from './models/treeItems';
import { PrompterTreeProvider } from './providers/prompterTreeProvider';
import { PromptGenerator } from './utils/promptGenerator';
// TreeView provider for the sidebar
class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<FileTreeItem | undefined | null | void> = new vscode.EventEmitter<FileTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<FileTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
private selectedFiles: Set<string> = new Set();
private xmlEditsEnabled: boolean = false;
constructor(private workspaceRoot: string | undefined) {}
getTreeItem(element: FileTreeItem): vscode.TreeItem {
const item = element;
if (this.selectedFiles.has(element.resourceUri.fsPath)) {
item.checkboxState = vscode.TreeItemCheckboxState.Checked;
item.label = path.basename(element.resourceUri.fsPath);
} else {
item.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
item.label = path.basename(element.resourceUri.fsPath);
}
return item;
}
async getChildren(element?: FileTreeItem): Promise<FileTreeItem[]> {
if (!this.workspaceRoot) {
vscode.window.showInformationMessage('No workspace folder is opened');
return Promise.resolve([]);
}
if (element) {
const dirPath = element.resourceUri.fsPath;
return this.getFilesInDirectory(dirPath);
} else {
return this.getFilesInDirectory(this.workspaceRoot);
}
}
private async getFilesInDirectory(dirPath: string): Promise<FileTreeItem[]> {
try {
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
return Promise.all(files.map(async ([name, type]) => {
const filePath = path.join(dirPath, name);
const uri = vscode.Uri.file(filePath);
const isDirectory = (type & vscode.FileType.Directory) !== 0;
return new FileTreeItem(
uri,
isDirectory ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None,
isDirectory
);
}));
} catch (error) {
console.error(`Error reading directory ${dirPath}:`, error);
return [];
}
}
toggleSelection(item: FileTreeItem): void {
const filePath = item.resourceUri.fsPath;
console.log('Toggle selection called for:', filePath);
if (this.selectedFiles.has(filePath)) {
this.removeFromSelection(item);
} else {
this.addToSelection(item);
}
}
addToSelection(item: FileTreeItem): void {
const filePath = item.resourceUri.fsPath;
this.selectedFiles.add(filePath);
console.log('Added file to selection, count:', this.selectedFiles.size);
this._onDidChangeTreeData.fire();
}
removeFromSelection(item: FileTreeItem): void {
const filePath = item.resourceUri.fsPath;
this.selectedFiles.delete(filePath);
console.log('Removed file from selection, count:', this.selectedFiles.size);
this._onDidChangeTreeData.fire();
}
getSelectedFiles(): Set<string> {
console.log('getSelectedFiles called, count:', this.selectedFiles.size);
return this.selectedFiles;
}
toggleXmlEdits(): void {
this.xmlEditsEnabled = !this.xmlEditsEnabled;
}
isXmlEditsEnabled(): boolean {
return this.xmlEditsEnabled;
}
refresh(): void {
this._onDidChangeTreeData.fire();
}
}
// Utility function to estimate tokens in text
function estimateTokens(text: string): number {
// Rough estimation: Split by whitespace and punctuation
// This is a simple approximation, actual token count may vary by model
const words = text.split(/[\s\p{P}]+/u).filter(Boolean);
return Math.ceil(words.length * 1.3); // Add 30% overhead for special tokens
}
// Function to read file content
async function readFileContent(filePath: string): Promise<string> {
try {
const readData = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath));
return Buffer.from(readData).toString('utf8');
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
return '';
}
}
// Function to generate XML prompt
function generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, xmlEdits: boolean): string {
const xmlParts = ['<?xml version="1.0" encoding="UTF-8"?>', '<prompt>'];
// Add files section
xmlParts.push(' <files>');
for (const [path, { content, tokens }] of files) {
xmlParts.push(' <file>');
xmlParts.push(` <path>${path}</path>`);
xmlParts.push(` <tokens>${tokens}</tokens>`);
xmlParts.push(` <content><![CDATA[${content}]]></content>`);
xmlParts.push(' </file>');
}
xmlParts.push(' </files>');
// Add XML edits flag if enabled
if (xmlEdits) {
xmlParts.push(' <options>');
xmlParts.push(' <xml_edits>true</xml_edits>');
xmlParts.push(' </options>');
}
xmlParts.push('</prompt>');
return xmlParts.join('\n');
}
// This method is called when your extension is activated
/**
* This method is called when the extension is activated
*/
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "prompter" is now active!');
console.log('Prompter extension is now active!');
// Get the workspace folder
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
console.log('Workspace root:', workspaceRoot);
// Create the tree data provider
console.log('Creating tree data provider...');
const prompterTreeProvider = new PrompterTreeProvider(workspaceRoot);
console.log('Tree data provider created successfully');
// Register the TreeView with checkbox support
const treeView = vscode.window.createTreeView('prompterView', {
treeDataProvider: prompterTreeProvider,
showCollapseAll: true,
canSelectMany: true
});
console.log('Registering tree view with ID: prompterView');
let treeView: vscode.TreeView<FileTreeItem | SettingTreeItem>;
try {
treeView = vscode.window.createTreeView('prompterView', {
treeDataProvider: prompterTreeProvider,
showCollapseAll: true,
canSelectMany: true
});
console.log('Tree view registered successfully');
} catch (error) {
console.error('Error registering tree view:', error);
// Create a fallback empty tree view to prevent further errors
treeView = {} as vscode.TreeView<FileTreeItem | SettingTreeItem>;
}
// Handle checkbox changes
treeView.onDidChangeCheckboxState(e => {
treeView.onDidChangeCheckboxState((e: vscode.TreeCheckboxChangeEvent<FileTreeItem | SettingTreeItem>) => {
console.log('Checkbox state changed');
for (const [item, state] of e.items) {
const fileItem = item as FileTreeItem;
console.log(`Checkbox changed for ${fileItem.resourceUri.fsPath} to ${state}`);
if (state === vscode.TreeItemCheckboxState.Checked) {
prompterTreeProvider.addToSelection(fileItem);
} else {
prompterTreeProvider.removeFromSelection(fileItem);
if (item instanceof FileTreeItem) {
console.log(`Checkbox changed for ${item.resourceUri.fsPath} to ${state}`);
if (state === vscode.TreeItemCheckboxState.Checked) {
prompterTreeProvider.addToSelection(item);
} else {
prompterTreeProvider.removeFromSelection(item);
}
} else if (item instanceof SettingTreeItem) {
// Handle settings checkbox changes
console.log(`Setting changed: ${item.settingKey} to ${state === vscode.TreeItemCheckboxState.Checked}`);
prompterTreeProvider.updateSetting(
item.settingKey,
state === vscode.TreeItemCheckboxState.Checked
);
// Update formatting instructions button text if that setting changed
if (item.settingKey === 'includeFormattingInstructions') {
xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ?
"$(check) Formatting Instructions" : "$(diff-added) XML Edits";
}
}
}
});
// Create XML edits toggle button
const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
// Create formatting instructions toggle button
const xmlEditsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1);
xmlEditsButton.text = "$(diff-added) XML Edits";
xmlEditsButton.tooltip = "Toggle XML Edits mode";
xmlEditsButton.tooltip = "Toggle formatting instructions mode";
xmlEditsButton.command = 'prompter.toggleXmlEdits';
xmlEditsButton.show();
// Create copy button
const copyButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
const copyButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 3);
copyButton.text = "$(clippy) Copy";
copyButton.tooltip = "Generate and copy XML prompt";
copyButton.tooltip = "Generate and copy prompt";
copyButton.command = 'prompter.generatePrompt';
copyButton.show();
// Create settings button
const settingsButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 2);
settingsButton.text = "$(settings-gear)";
settingsButton.tooltip = "Prompter Settings";
settingsButton.command = 'prompter.openSettings';
settingsButton.show();
// Register command to toggle file selection
let toggleSelectionCommand = vscode.commands.registerCommand('prompter.toggleSelection', (item: FileTreeItem) => {
console.log('Toggle selection command triggered for:', item.resourceUri.fsPath);
prompterTreeProvider.toggleSelection(item);
if (item.resourceUri) {
console.log('Toggle selection command triggered for:', item.resourceUri.fsPath);
prompterTreeProvider.toggleSelection(item);
}
});
// Register command to toggle XML edits
@@ -213,54 +101,73 @@ export function activate(context: vscode.ExtensionContext) {
xmlEditsButton.text = prompterTreeProvider.isXmlEditsEnabled() ?
"$(check) XML Edits" : "$(diff-added) XML Edits";
});
// Register command to open settings
let openSettingsCommand = vscode.commands.registerCommand('prompter.openSettings', () => {
prompterTreeProvider.toggleSettingsView();
// Update the settings button icon based on current view
settingsButton.text = prompterTreeProvider.isShowingSettings() ?
"$(list-tree)" : "$(settings-gear)";
settingsButton.tooltip = prompterTreeProvider.isShowingSettings() ?
"Show Files" : "Prompter Settings";
});
// Register command to show the tree view
let openTreeViewCommand = vscode.commands.registerCommand('prompter.openTreeView', () => {
prompterTreeProvider.showFilesView();
});
// Register command to generate prompt from selected files
let generatePromptCommand = vscode.commands.registerCommand('prompter.generatePrompt', async () => {
const selectedFiles = prompterTreeProvider.getSelectedFiles();
console.log('Generate prompt command triggered, selected files:', [...selectedFiles]);
if (selectedFiles.size === 0) {
vscode.window.showInformationMessage('Please select files first');
return;
}
let totalTokens = 0;
const fileContents = new Map<string, { content: string; tokens: number }>();
// Process each selected file
for (const filePath of selectedFiles) {
const content = await readFileContent(filePath);
const tokens = estimateTokens(content);
totalTokens += tokens;
fileContents.set(filePath, { content, tokens });
// Show individual file token count
vscode.window.showInformationMessage(
`File: ${filePath}\nEstimated tokens: ${tokens}`
try {
// Generate the prompt using our utility class
const promptText = await PromptGenerator.generatePrompt(
selectedFiles,
prompterTreeProvider.getSettings()
);
// Check if we got a valid prompt text
if (promptText === null) {
// Show warning if all files were filtered out
vscode.window.showWarningMessage('All selected files were filtered out by ignore patterns');
return;
}
// Copy to clipboard only if we have valid content
await vscode.env.clipboard.writeText(promptText);
vscode.window.showInformationMessage('Prompt copied to clipboard!');
} catch (error) {
console.error('Error generating prompt:', error);
vscode.window.showErrorMessage('Error generating prompt');
}
// Show total token count
vscode.window.showInformationMessage(
`Total estimated tokens for ${selectedFiles.size} files: ${totalTokens}`
);
// Generate XML prompt
const xmlPrompt = generateXMLPrompt(fileContents, prompterTreeProvider.isXmlEditsEnabled());
// Copy to clipboard
await vscode.env.clipboard.writeText(xmlPrompt);
vscode.window.showInformationMessage('XML prompt copied to clipboard!');
});
// Add all disposables to context subscriptions
context.subscriptions.push(
treeView,
xmlEditsButton,
copyButton,
settingsButton,
toggleSelectionCommand,
toggleXmlEditsCommand,
generatePromptCommand
generatePromptCommand,
openSettingsCommand,
openTreeViewCommand
);
}
// This method is called when your extension is deactivated
export function deactivate() {}
/**
* This method is called when the extension is deactivated
*/
export function deactivate() {
// Clean up resources when the extension is deactivated
}

13
src/models/settings.ts Normal file
View File

@@ -0,0 +1,13 @@
// Settings interface for the Prompter extension
export interface PrompterSettings {
includeFormattingInstructions: boolean;
tokenCalculationEnabled: boolean;
includeFileMap: boolean;
}
// Default settings values
export const DEFAULT_SETTINGS: PrompterSettings = {
includeFormattingInstructions: false,
tokenCalculationEnabled: true,
includeFileMap: true
};

32
src/models/treeItems.ts Normal file
View File

@@ -0,0 +1,32 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { PrompterSettings } from './settings';
// Tree item for files in the explorer
export class FileTreeItem extends vscode.TreeItem {
constructor(
public readonly resourceUri: vscode.Uri,
public readonly collapsibleState: vscode.TreeItemCollapsibleState
) {
super(resourceUri, collapsibleState);
this.tooltip = resourceUri.fsPath;
this.description = path.basename(resourceUri.fsPath);
this.contextValue = 'file';
// Set the checkbox state to unchecked by default
this.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
}
}
// Tree item for settings in the settings view
export class SettingTreeItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly settingKey: keyof PrompterSettings,
public readonly checked: boolean
) {
super(label, vscode.TreeItemCollapsibleState.None);
this.checkboxState = checked ? vscode.TreeItemCheckboxState.Checked : vscode.TreeItemCheckboxState.Unchecked;
this.contextValue = 'setting';
}
}

View File

@@ -0,0 +1,209 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
// Use require for ignore package with proper fallback
let ignoreFunc: () => any;
try {
const ignoreModule = require('ignore');
if (typeof ignoreModule === 'function') {
ignoreFunc = ignoreModule;
} else if (ignoreModule && typeof ignoreModule.default === 'function') {
ignoreFunc = ignoreModule.default;
} else {
throw new Error('Ignore module is neither a function nor has a default function');
}
console.log('Successfully loaded ignore function in FileSelectionManager');
} catch (error) {
console.error('Error loading ignore package in FileSelectionManager:', error);
ignoreFunc = () => ({
add: () => {},
ignores: () => false
});
}
/**
* Manages file selection state for the Prompter extension
*/
export class FileSelectionManager {
private selectedFiles: Set<string> = new Set();
constructor() {}
/**
* Get the path relative to the workspace root
* @param filePath Absolute file path
* @param rootPath Workspace root path
* @returns Relative path
*/
private getRelativePath(filePath: string, rootPath: string): string {
if (filePath.startsWith(rootPath)) {
const relativePath = filePath.substring(rootPath.length);
return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
}
return filePath;
}
/**
* Check if a path or any of its parent directories are ignored
* @param pathToCheck The path to check
* @param ig The ignore instance
* @param workspaceRoot The workspace root path
* @returns True if the path or any parent is ignored
*/
private isPathIgnored(pathToCheck: string, ig: any, workspaceRoot: string): boolean {
let currentPath = pathToCheck;
while (currentPath !== workspaceRoot) {
const relativePath = this.getRelativePath(currentPath, workspaceRoot);
const normalizedPath = relativePath.split(path.sep).join('/');
const isIgnored = ig.ignores(normalizedPath);
console.log(`Checking ${normalizedPath}: ignored = ${isIgnored}`);
if (isIgnored) {
console.log(`Path ${pathToCheck} ignored because parent ${normalizedPath} is ignored`);
return true;
}
currentPath = path.dirname(currentPath);
if (currentPath === workspaceRoot) {
break;
}
}
return false;
}
/**
* Load ignore patterns from .gitignore files in the given directory and its parent directories
* @param directoryPath The directory path to start searching from
* @param workspaceRoot The workspace root path
* @returns An ignore instance with loaded patterns
*/
private loadIgnorePatternsFromDirectory(directoryPath: string, workspaceRoot: string): any {
const ig = ignoreFunc();
let currentDir = directoryPath;
// Check for .gitignore in the current directory and all parent directories up to workspace root
while (currentDir.startsWith(workspaceRoot)) {
try {
const gitignorePath = path.join(currentDir, '.gitignore');
if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
ig.add(gitignoreContent);
console.log(`Loaded .gitignore patterns from ${gitignorePath}`);
}
} catch (error) {
console.error(`Error loading .gitignore from ${currentDir}:`, error);
}
// Stop if we've reached the workspace root
if (currentDir === workspaceRoot) {
break;
}
// Move up to parent directory
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) { // Avoid infinite loop if we've reached the root
break;
}
currentDir = parentDir;
}
return ig;
}
/**
* Add a file to the selection if it's not ignored
*/
addFile(filePath: string): void {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
const dirPath = path.dirname(filePath);
const ig = this.loadIgnorePatternsFromDirectory(dirPath, workspaceRoot);
if (this.isPathIgnored(filePath, ig, workspaceRoot)) {
console.log(`Ignoring file ${filePath} because it or a parent directory is ignored`);
return;
}
this.selectedFiles.add(filePath);
console.log(`Added ${filePath} to selection`);
}
/**
* Add a directory and all its non-ignored contents to the selection
*/
async addDirectory(dirPath: string): Promise<void> {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
const ig = this.loadIgnorePatternsFromDirectory(dirPath, workspaceRoot);
const relativeDirPath = this.getRelativePath(dirPath, workspaceRoot);
const normalizedDirPath = relativeDirPath.split(path.sep).join('/');
if (ig.ignores(normalizedDirPath)) {
console.log(`Directory ${dirPath} is ignored, skipping its contents`);
return;
}
this.selectedFiles.add(dirPath);
console.log(`Added directory ${dirPath} to selection`);
try {
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
for (const [name, type] of files) {
const filePath = path.join(dirPath, name);
const relativeFilePath = this.getRelativePath(filePath, workspaceRoot);
const normalizedFilePath = relativeFilePath.split(path.sep).join('/');
if (type === vscode.FileType.Directory) {
await this.addDirectory(filePath);
} else if (!ig.ignores(normalizedFilePath)) {
this.selectedFiles.add(filePath);
console.log(`Added ${filePath} to selection`);
} else {
console.log(`Ignoring ${filePath} due to ignore patterns`);
}
}
} catch (error) {
console.error(`Error adding directory ${dirPath}:`, error);
}
}
/**
* Remove a file from the selection
*/
removeFile(filePath: string): void {
this.selectedFiles.delete(filePath);
console.log(`Removed ${filePath} from selection`);
}
/**
* Remove a directory and all its contents from the selection
*/
async removeDirectory(dirPath: string): Promise<void> {
try {
this.selectedFiles.delete(dirPath);
console.log(`Removed directory ${dirPath} from selection`);
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
for (const [name, type] of files) {
const filePath = path.join(dirPath, name);
if (type === vscode.FileType.Directory) {
await this.removeDirectory(filePath);
} else {
this.selectedFiles.delete(filePath);
console.log(`Removed ${filePath} from selection (from directory)`);
}
}
} catch (error) {
console.error(`Error removing directory from selection: ${dirPath}`, error);
}
}
/**
* Check if a file is selected
*/
isSelected(filePath: string): boolean {
return this.selectedFiles.has(filePath);
}
/**
* Get all selected files
*/
getSelectedFiles(): Set<string> {
console.log('getSelectedFiles called, count:', this.selectedFiles.size);
return this.selectedFiles;
}
}

View File

@@ -0,0 +1,211 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { FileTreeItem, SettingTreeItem } from '../models/treeItems';
import { PrompterSettings } from '../models/settings';
import { FileSelectionManager } from './fileSelectionManager';
import { SettingsManager } from './settingsManager';
/**
* Tree provider for the Prompter extension
* Handles both file browsing and settings views
*/
export class PrompterTreeProvider implements vscode.TreeDataProvider<FileTreeItem | SettingTreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<FileTreeItem | SettingTreeItem | undefined | null | void> =
new vscode.EventEmitter<FileTreeItem | SettingTreeItem | undefined | null | void>();
readonly onDidChangeTreeData: vscode.Event<FileTreeItem | SettingTreeItem | undefined | null | void> =
this._onDidChangeTreeData.event;
private fileSelectionManager: FileSelectionManager;
private settingsManager: SettingsManager;
private showingSettings: boolean = false;
constructor(private workspaceRoot: string | undefined) {
this.fileSelectionManager = new FileSelectionManager();
this.settingsManager = new SettingsManager();
}
// Toggle between file view and settings view
toggleSettingsView(): void {
this.showingSettings = !this.showingSettings;
this.refresh();
}
showSettingsView(): void {
this.showingSettings = true;
this.refresh();
}
showFilesView(): void {
this.showingSettings = false;
this.refresh();
}
getTreeItem(element: FileTreeItem | SettingTreeItem): vscode.TreeItem {
// Return the element as is if it's a SettingTreeItem
if (element instanceof SettingTreeItem) {
return element;
}
// Handle FileTreeItem
const fileItem = element as FileTreeItem;
if (fileItem.resourceUri && this.fileSelectionManager.isSelected(fileItem.resourceUri.fsPath)) {
fileItem.checkboxState = vscode.TreeItemCheckboxState.Checked;
} else {
fileItem.checkboxState = vscode.TreeItemCheckboxState.Unchecked;
}
return fileItem;
}
async getChildren(element?: FileTreeItem | SettingTreeItem): Promise<(FileTreeItem | SettingTreeItem)[]> {
// If we're showing settings, return settings items
if (this.showingSettings && !element) {
return this.getSettingsItems();
}
// Otherwise show files
if (!this.workspaceRoot) {
vscode.window.showInformationMessage('No workspace folder is opened');
return Promise.resolve([]);
}
if (element) {
// Make sure we're dealing with a FileTreeItem that has a resourceUri
if (!(element instanceof SettingTreeItem) && element.resourceUri) {
return this.getFilesInDirectory(element.resourceUri.fsPath);
}
return Promise.resolve([]);
} else {
return this.getFilesInDirectory(this.workspaceRoot);
}
}
private async getFilesInDirectory(dirPath: string): Promise<FileTreeItem[]> {
try {
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(dirPath));
// Sort by type first (directories first), then alphabetically by name
const sortedFiles = [...files].sort((a, b) => {
const [nameA, typeA] = a;
const [nameB, typeB] = b;
// If types are different, directories (type 2) come before files (type 1)
if (typeA !== typeB) {
return typeB - typeA; // Descending order by type puts directories first
}
// If types are the same, sort alphabetically by name
return nameA.localeCompare(nameB, undefined, { sensitivity: 'base' });
});
return sortedFiles.map(([name, type]) => {
const filePath = path.join(dirPath, name);
const uri = vscode.Uri.file(filePath);
return new FileTreeItem(
uri,
type === vscode.FileType.Directory
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None
);
});
} catch (error) {
console.error(`Error reading directory: ${dirPath}`, error);
return [];
}
}
// Refresh the tree view
refresh(): void {
this._onDidChangeTreeData.fire();
}
// Add a file to the selection
addToSelection(item: FileTreeItem): void {
if (item.resourceUri) {
if (this.isDirectory(item)) {
this.fileSelectionManager.addDirectory(item.resourceUri.fsPath);
} else {
this.fileSelectionManager.addFile(item.resourceUri.fsPath);
}
}
}
// Remove a file from the selection
removeFromSelection(item: FileTreeItem): void {
if (item.resourceUri) {
if (this.isDirectory(item)) {
this.fileSelectionManager.removeDirectory(item.resourceUri.fsPath);
} else {
this.fileSelectionManager.removeFile(item.resourceUri.fsPath);
}
}
}
private isDirectory(item: FileTreeItem): boolean {
return item.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed ||
item.collapsibleState === vscode.TreeItemCollapsibleState.Expanded;
}
// Toggle a file's selection status
toggleSelection(item: FileTreeItem): void {
if (item.resourceUri) {
const filePath = item.resourceUri.fsPath;
if (this.fileSelectionManager.isSelected(filePath)) {
this.removeFromSelection(item);
} else {
this.addToSelection(item);
}
this.refresh();
}
}
// Get the selected files
getSelectedFiles(): Set<string> {
return this.fileSelectionManager.getSelectedFiles();
}
// Get settings items for the tree view
private getSettingsItems(): SettingTreeItem[] {
const settings = this.settingsManager.getSettings();
return [
new SettingTreeItem('Include Formatting Instructions', 'includeFormattingInstructions', settings.includeFormattingInstructions),
new SettingTreeItem('Token Calculation', 'tokenCalculationEnabled', settings.tokenCalculationEnabled),
new SettingTreeItem('Include File Map', 'includeFileMap', settings.includeFileMap)
];
}
// Update a setting value
updateSetting(key: keyof PrompterSettings, value: boolean): void {
this.settingsManager.updateSetting(key, value);
this.refresh();
}
// Check if showing settings view
isShowingSettings(): boolean {
return this.showingSettings;
}
// Toggle formatting instructions setting
toggleXmlEdits(): void {
this.settingsManager.toggleFormattingInstructions();
this.refresh();
}
// Check if formatting instructions are enabled
isXmlEditsEnabled(): boolean {
return this.settingsManager.isFormattingInstructionsEnabled();
}
// Get all settings
getSettings(): PrompterSettings {
return this.settingsManager.getSettings();
}
// Update all settings at once
updateSettings(newSettings: PrompterSettings): void {
this.settingsManager.updateAllSettings(newSettings);
this.refresh();
}
}

View File

@@ -0,0 +1,46 @@
import { PrompterSettings, DEFAULT_SETTINGS } from '../models/settings';
/**
* Manages settings for the Prompter extension
*/
export class SettingsManager {
private settings: PrompterSettings = { ...DEFAULT_SETTINGS };
constructor() {}
/**
* Update a specific setting
*/
updateSetting(key: keyof PrompterSettings, value: boolean): void {
this.settings[key] = value;
}
/**
* Toggle formatting instructions setting
*/
toggleFormattingInstructions(): void {
this.settings.includeFormattingInstructions = !this.settings.includeFormattingInstructions;
}
/**
* Check if formatting instructions are enabled
*/
isFormattingInstructionsEnabled(): boolean {
return this.settings.includeFormattingInstructions;
}
/**
* Get all settings
*/
getSettings(): PrompterSettings {
return { ...this.settings };
}
/**
* Update all settings at once
*/
updateAllSettings(newSettings: PrompterSettings): void {
this.settings = { ...newSettings };
}
}

33
src/test/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as path from 'path';
import * as fs from 'fs';
import Mocha from 'mocha';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise<void>((resolve, reject) => {
try {
// Simple approach - add extension.test.js directly
// This will find our basic test file
mocha.addFile(path.resolve(testsRoot, 'test/extension.test.js'));
// Run the mocha test
mocha.run((failures: number) => {
if (failures > 0) {
reject(new Error(`${failures} tests failed.`));
} else {
resolve();
}
});
} catch (err) {
console.error(err);
reject(err);
}
});
}

59
src/test/runTest.mjs Normal file
View File

@@ -0,0 +1,59 @@
import { runTests } from '@vscode/test-electron';
import * as path from 'path';
import { fileURLToPath } from 'url';
import * as os from 'os';
import * as fs from 'fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function main() {
try {
// Create a temporary test workspace
// Using the system temp directory instead of external volume
const tmpDir = os.tmpdir();
const testWorkspaceDir = path.join(tmpDir, `vscode-test-workspace-${Math.random().toString(36).substring(2)}`);
const userDataDir = path.join(tmpDir, `vscode-test-user-data-${Math.random().toString(36).substring(2)}`);
// Ensure the test workspace directory exists
if (!fs.existsSync(testWorkspaceDir)) {
fs.mkdirSync(testWorkspaceDir, { recursive: true });
}
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to the extension test script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, '../../out/test');
console.log('Running tests with the following configuration:');
console.log(`Extension Development Path: ${extensionDevelopmentPath}`);
console.log(`Extension Tests Path: ${extensionTestsPath}`);
console.log(`Workspace Dir: ${testWorkspaceDir}`);
console.log(`User Data Dir: ${userDataDir}`);
// Download VS Code, unzip it and run the integration test
await runTests({
version: '1.98.0', // Specify the exact version your extension is built for
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: [
testWorkspaceDir,
'--disable-extensions',
`--user-data-dir=${userDataDir}`,
'--skip-getting-started',
'--skip-release-notes',
'--disable-telemetry',
'--disable-updates',
'--disable-crash-reporter',
'--disable-workspace-trust'
]
});
} catch (err) {
console.error('Failed to run tests', err);
process.exit(1);
}
}
main();

21
src/utils/fileReader.ts Normal file
View File

@@ -0,0 +1,21 @@
import * as vscode from 'vscode';
/**
* Utility for reading file contents
*/
export class FileReader {
/**
* Read the content of a file
* @param filePath Path to the file to read
* @returns The file content as a string
*/
static async readFileContent(filePath: string): Promise<string> {
try {
const readData = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath));
return Buffer.from(readData).toString('utf8');
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
return '';
}
}
}

View File

@@ -0,0 +1,291 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { PrompterSettings } from '../models/settings';
import { FileReader } from './fileReader';
import { TokenEstimator } from './tokenEstimator';
// Use require for ignore package with proper fallback
let ignoreFunc: () => any;
try {
const ignoreModule = require('ignore');
if (typeof ignoreModule === 'function') {
ignoreFunc = ignoreModule;
} else if (ignoreModule && typeof ignoreModule.default === 'function') {
ignoreFunc = ignoreModule.default;
} else {
throw new Error('Ignore module is neither a function nor has a default function');
}
console.log('Successfully loaded ignore function');
} catch (error) {
console.error('Error loading ignore package:', error);
ignoreFunc = () => ({
add: () => {},
ignores: () => false
});
}
export class PromptGenerator {
/**
* Generate a prompt from the selected files
* @param selectedFiles Set of file paths to include in the prompt
* @param settings Settings to apply when generating the prompt
* @returns The generated prompt text
*/
static async generatePrompt(selectedFiles: Set<string>, settings: PrompterSettings): Promise<string | null> {
if (selectedFiles.size === 0) {
throw new Error('No files selected');
}
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
// Create ignore instance using the resolved function
const ig = ignoreFunc();
console.log('Created ignore instance');
// Load .gitignore patterns
try {
const gitignorePath = path.join(workspaceRoot, '.gitignore');
if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
ig.add(gitignoreContent);
console.log('Successfully loaded .gitignore patterns:', gitignoreContent.split('\n').filter(Boolean));
} else {
console.log('No .gitignore file found at:', gitignorePath);
}
} catch (error) {
console.error('Error loading .gitignore:', error);
}
const fileContents = new Map<string, { content: string; tokens: number }>();
const filteredFiles = new Set<string>();
let totalTokens = 0;
// Process and filter files
for (const filePath of selectedFiles) {
try {
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
console.log(`Skipping directory: ${filePath}`);
continue;
}
} catch (error) {
console.error(`Error checking file stats for ${filePath}:`, error);
continue;
}
const relativePath = this.getRelativePath(filePath, workspaceRoot);
const normalizedPath = relativePath.split(path.sep).join('/');
console.log(`Checking path: ${normalizedPath}`);
if (ig.ignores(normalizedPath)) {
console.log(`Ignoring file based on patterns: ${normalizedPath}`);
continue;
}
console.log(`Processing file: ${normalizedPath}`);
filteredFiles.add(filePath);
const content = await FileReader.readFileContent(filePath);
const tokens = TokenEstimator.estimateTokens(content);
totalTokens += tokens;
fileContents.set(filePath, { content, tokens });
}
if (filteredFiles.size === 0) {
// Return null to signal that no files were available after filtering
return null;
}
// Create filtered contents map
const filteredContents = new Map<string, { content: string; tokens: number }>();
for (const filePath of filteredFiles) {
if (fileContents.has(filePath)) {
filteredContents.set(filePath, fileContents.get(filePath)!);
}
}
return this.generateXMLPrompt(filteredContents, settings);
}
/**
* Generate an XML formatted prompt following the new schema format
* @param files Map of file paths to content and token counts
* @param settings Settings to apply
* @returns XML formatted prompt
*/
private static generateXMLPrompt(files: Map<string, { content: string; tokens: number }>, settings: PrompterSettings): string {
const xmlParts: string[] = [];
let formattingInstructions = '';
if (settings.includeFormattingInstructions) {
try {
const extensionPath = vscode.extensions.getExtension('prompter')?.extensionPath ||
path.join(__dirname, '..', '..');
const formattingInstructionsPath = path.join(extensionPath, 'resources', 'xml_formatting_instructions.xml');
if (fs.existsSync(formattingInstructionsPath)) {
formattingInstructions = fs.readFileSync(formattingInstructionsPath, 'utf8');
} else {
console.warn('XML formatting instructions file not found at:', formattingInstructionsPath);
}
} catch (error) {
console.error('Error reading XML formatting instructions:', error);
}
}
if (settings.includeFileMap) {
xmlParts.push('<file_map>');
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
xmlParts.push(workspaceRoot);
const fileTree = this.generateFileTree(files, workspaceRoot);
xmlParts.push(fileTree);
xmlParts.push('</file_map>');
}
xmlParts.push('<file_contents>');
for (const [filePath, { content, tokens }] of files) {
const extension = path.extname(filePath);
let language = extension.substring(1);
if (extension === '.js' || extension === '.jsx') {
language = 'javascript';
} else if (extension === '.ts' || extension === '.tsx') {
language = 'typescript';
} else if (extension === '.md') {
language = 'md';
} else if (extension === '.py') {
language = 'python';
} else if (extension === '.html') {
language = 'html';
} else if (extension === '.css') {
language = 'css';
} else if (extension === '.json') {
language = 'json';
}
const formattedContent = content;
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath || '';
xmlParts.push(`File: ${this.getRelativePath(filePath, workspaceRoot)}`);
xmlParts.push(`\`\`\`${language}`);
xmlParts.push(formattedContent);
xmlParts.push('\`\`\`');
}
xmlParts.push('</file_contents>');
if (settings.tokenCalculationEnabled) {
const totalTokens = Array.from(files.values()).reduce((sum, { tokens }) => sum + tokens, 0);
vscode.window.showInformationMessage(`Total tokens: ${totalTokens}`);
}
if (settings.includeFormattingInstructions && formattingInstructions) {
xmlParts.push(formattingInstructions);
}
return xmlParts.join('\n');
}
/**
* Generate a tree representation of files
* @param files Map of file paths
* @param rootPath The workspace root path
* @returns String representation of the file tree
*/
private static generateFileTree(files: Map<string, any>, rootPath: string): string {
const treeLines: string[] = [];
interface TreeNode {
name: string;
isDirectory: boolean;
children: Map<string, TreeNode>;
}
const root: TreeNode = {
name: path.basename(rootPath),
isDirectory: true,
children: new Map<string, TreeNode>()
};
const ig = ignoreFunc();
try {
const gitignorePath = path.join(rootPath, '.gitignore');
if (fs.existsSync(gitignorePath)) {
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
ig.add(gitignoreContent);
ig.add('!.gitignore');
}
} catch (error) {
console.error('Error reading .gitignore:', error);
}
for (const filePath of files.keys()) {
const relativePath = this.getRelativePath(filePath, rootPath);
const normalizedPath = relativePath.split(path.sep).join('/');
if (ig.ignores(normalizedPath)) {
continue;
}
const parts = relativePath.split('/');
let currentNode = root;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isDirectory = i < parts.length - 1;
if (!currentNode.children.has(part)) {
currentNode.children.set(part, {
name: part,
isDirectory,
children: new Map<string, TreeNode>()
});
} else if (isDirectory) {
currentNode.children.get(part)!.isDirectory = true;
}
currentNode = currentNode.children.get(part)!;
}
}
const buildTreeLines = (node: TreeNode, prefix: string = '', isLast: boolean = true, parentPrefix: string = ''): void => {
if (node !== root) {
const linePrefix = parentPrefix + (isLast ? '└── ' : '├── ');
treeLines.push(`${linePrefix}${node.name}${node.isDirectory ? '' : ''}`);
}
const sortedChildren = Array.from(node.children.values())
.sort((a, b) => {
if (a.isDirectory === b.isDirectory) {
return a.name.localeCompare(b.name);
}
return a.isDirectory ? -1 : 1;
});
sortedChildren.forEach((child, index) => {
const isChildLast = index === sortedChildren.length - 1;
const childParentPrefix = node === root ? '' : parentPrefix + (isLast ? ' ' : '│ ');
buildTreeLines(child, prefix, isChildLast, childParentPrefix);
});
};
buildTreeLines(root);
return treeLines.join('\n');
}
/**
* Get the path relative to the workspace root
* @param filePath Absolute file path
* @param rootPath Workspace root path
* @returns Relative path
*/
private static getRelativePath(filePath: string, rootPath: string): string {
if (filePath.startsWith(rootPath)) {
const relativePath = filePath.substring(rootPath.length);
return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
}
return filePath;
}
}

View File

@@ -0,0 +1,16 @@
/**
* Utility for estimating token counts in text
*/
export class TokenEstimator {
/**
* Estimate the number of tokens in a text string
* @param text The text to estimate tokens for
* @returns Estimated token count
*/
static estimateTokens(text: string): number {
// Rough estimation: Split by whitespace and punctuation
// This is a simple approximation, actual token count may vary by model
const words = text.split(/[\s\p{P}]+/u).filter(Boolean);
return Math.ceil(words.length * 1.3); // Add 30% overhead for special tokens
}
}

18
tsconfig.webpack.json Normal file
View File

@@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "dist",
"sourceMap": true,
"esModuleInterop": true
},
"exclude": [
"node_modules",
"src/test/**",
"**/*.test.ts"
],
"include": [
"src/**/*.ts"
]
}

46
webpack.config.js Normal file
View File

@@ -0,0 +1,46 @@
const path = require('path');
const webpack = require('webpack');
/**
* @type {import('webpack').Configuration}
*/
const config = {
target: 'node',
mode: 'none', // Set to 'production' for minified output
entry: {
main: './src/extension.ts'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2',
devtoolModuleFilenameTemplate: '../[resource-path]'
},
devtool: 'source-map',
externals: {
vscode: 'commonjs vscode' // The vscode module is created on-the-fly and must be excluded
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules|src\/test/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
configFile: path.resolve(__dirname, './tsconfig.webpack.json')
}
}
]
}
]
}
};
module.exports = config;