Compare commits
16 Commits
developmen
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| e34ba394b9 | |||
| 43308dfbe1 | |||
|
|
e8904ea1ce | ||
|
|
3d25fe0f04 | ||
|
|
d91957b945 | ||
|
|
923f8c24e7 | ||
|
|
40ad68e0ff | ||
|
|
1762387301 | ||
| ea9286687d | |||
|
|
cc837a1427 | ||
|
|
154c08411c | ||
| 1870f2a7ce | |||
|
|
ff92f6eff2 | ||
|
|
eeb5e207f2 | ||
|
|
09b595948d | ||
|
|
63c0b81fc9 |
378
aiprompts/herolib_core/core_heroprompt.md
Normal file
378
aiprompts/herolib_core/core_heroprompt.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# HeroPrompt Module
|
||||
|
||||
The `heroprompt` module provides a hierarchical workspace-based system for organizing code files and generating structured AI prompts. It enables developers to select files from multiple directories and generate formatted prompts for AI code analysis.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Hierarchical Organization**: HeroPrompt → Workspace → Directory → Files
|
||||
- **Redis Persistence**: All data persists across sessions using Redis
|
||||
- **Factory Pattern**: Clean API with `get()`, `delete()`, `exists()`, `list()` functions
|
||||
- **File Selection**: Select specific files or entire directories for analysis
|
||||
- **Active Workspace**: Manage multiple workspaces with one active at a time
|
||||
- **Prompt Generation**: Generate structured prompts with file maps, contents, and instructions
|
||||
- **Template-Based**: Uses V templates for consistent prompt formatting
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Getting Started
|
||||
|
||||
```v
|
||||
import incubaid.herolib.develop.heroprompt
|
||||
|
||||
// Create or get a HeroPrompt instance
|
||||
mut hp := heroprompt.get(name: 'my_project', create: true)!
|
||||
|
||||
// Create a workspace (first workspace is automatically active)
|
||||
mut workspace := hp.new_workspace(
|
||||
name: 'my_workspace'
|
||||
description: 'My project workspace'
|
||||
)!
|
||||
```
|
||||
|
||||
### 2. Adding Directories
|
||||
|
||||
```v
|
||||
// Add directory and automatically scan all files
|
||||
mut dir := workspace.add_directory(
|
||||
path: '/path/to/your/code'
|
||||
name: 'backend'
|
||||
scan: true // Scans all files and subdirectories
|
||||
)!
|
||||
|
||||
// Add another directory
|
||||
mut frontend_dir := workspace.add_directory(
|
||||
path: '/path/to/frontend'
|
||||
name: 'frontend'
|
||||
scan: true
|
||||
)!
|
||||
```
|
||||
|
||||
### 3. Selecting Files
|
||||
|
||||
```v
|
||||
// Select specific files
|
||||
dir.select_file(path: '/path/to/your/code/main.v')!
|
||||
dir.select_file(path: '/path/to/your/code/utils.v')!
|
||||
|
||||
// Or select all files in a directory
|
||||
frontend_dir.select_all()!
|
||||
|
||||
// Deselect files
|
||||
dir.deselect_file(path: '/path/to/your/code/test.v')!
|
||||
|
||||
// Deselect all files
|
||||
dir.deselect_all()!
|
||||
```
|
||||
|
||||
### 4. Generating AI Prompts
|
||||
|
||||
```v
|
||||
// Generate prompt with selected files
|
||||
prompt := workspace.generate_prompt(
|
||||
instruction: 'Review these files and suggest improvements'
|
||||
)!
|
||||
|
||||
println(prompt)
|
||||
|
||||
// Or generate with specific files (overrides selection)
|
||||
prompt2 := workspace.generate_prompt(
|
||||
instruction: 'Analyze these specific files'
|
||||
selected_files: ['/path/to/file1.v', '/path/to/file2.v']
|
||||
)!
|
||||
```
|
||||
|
||||
## Factory Functions
|
||||
|
||||
### `heroprompt.get(name: string, create: bool) !HeroPrompt`
|
||||
|
||||
Gets or creates a HeroPrompt instance.
|
||||
|
||||
```v
|
||||
// Get existing instance or create new one
|
||||
mut hp := heroprompt.get(name: 'my_project', create: true)!
|
||||
|
||||
// Get existing instance only (error if doesn't exist)
|
||||
mut hp2 := heroprompt.get(name: 'my_project')!
|
||||
```
|
||||
|
||||
### `heroprompt.delete(name: string) !`
|
||||
|
||||
Deletes a HeroPrompt instance from Redis.
|
||||
|
||||
```v
|
||||
heroprompt.delete(name: 'my_project')!
|
||||
```
|
||||
|
||||
### `heroprompt.exists(name: string) !bool`
|
||||
|
||||
Checks if a HeroPrompt instance exists.
|
||||
|
||||
```v
|
||||
if heroprompt.exists(name: 'my_project')! {
|
||||
println('Instance exists')
|
||||
}
|
||||
```
|
||||
|
||||
### `heroprompt.list() ![]string`
|
||||
|
||||
Lists all HeroPrompt instance names.
|
||||
|
||||
```v
|
||||
instances := heroprompt.list()!
|
||||
for name in instances {
|
||||
println('Instance: ${name}')
|
||||
}
|
||||
```
|
||||
|
||||
## HeroPrompt Methods
|
||||
|
||||
### Workspace Management
|
||||
|
||||
#### `hp.new_workspace(name: string, description: string, is_active: bool) !&Workspace`
|
||||
|
||||
Creates a new workspace. The first workspace is automatically set as active.
|
||||
|
||||
```v
|
||||
mut ws := hp.new_workspace(
|
||||
name: 'backend'
|
||||
description: 'Backend API workspace'
|
||||
)!
|
||||
```
|
||||
|
||||
#### `hp.get_workspace(name: string) !&Workspace`
|
||||
|
||||
Retrieves an existing workspace by name.
|
||||
|
||||
```v
|
||||
mut ws := hp.get_workspace('backend')!
|
||||
```
|
||||
|
||||
#### `hp.get_active_workspace() !&Workspace`
|
||||
|
||||
Returns the currently active workspace.
|
||||
|
||||
```v
|
||||
mut active := hp.get_active_workspace()!
|
||||
println('Active workspace: ${active.name}')
|
||||
```
|
||||
|
||||
#### `hp.set_active_workspace(name: string) !`
|
||||
|
||||
Sets a workspace as active (deactivates all others).
|
||||
|
||||
```v
|
||||
hp.set_active_workspace('frontend')!
|
||||
```
|
||||
|
||||
#### `hp.list_workspaces() []&Workspace`
|
||||
|
||||
Lists all workspaces in the instance.
|
||||
|
||||
```v
|
||||
workspaces := hp.list_workspaces()
|
||||
for ws in workspaces {
|
||||
println('Workspace: ${ws.name}')
|
||||
}
|
||||
```
|
||||
|
||||
#### `hp.delete_workspace(name: string) !`
|
||||
|
||||
Deletes a workspace.
|
||||
|
||||
```v
|
||||
hp.delete_workspace('old_workspace')!
|
||||
```
|
||||
|
||||
## Workspace Methods
|
||||
|
||||
### Directory Management
|
||||
|
||||
#### `ws.add_directory(path: string, name: string, scan: bool) !&Directory`
|
||||
|
||||
Adds a directory to the workspace.
|
||||
|
||||
```v
|
||||
mut dir := ws.add_directory(
|
||||
path: '/path/to/code'
|
||||
name: 'my_code'
|
||||
scan: true // Automatically scans all files
|
||||
)!
|
||||
```
|
||||
|
||||
#### `ws.list_directories() []&Directory`
|
||||
|
||||
Lists all directories in the workspace.
|
||||
|
||||
```v
|
||||
dirs := ws.list_directories()
|
||||
for dir in dirs {
|
||||
println('Directory: ${dir.name}')
|
||||
}
|
||||
```
|
||||
|
||||
#### `ws.remove_directory(id: string) !`
|
||||
|
||||
Removes a directory from the workspace.
|
||||
|
||||
```v
|
||||
ws.remove_directory(id: dir.id)!
|
||||
```
|
||||
|
||||
### Prompt Generation
|
||||
|
||||
#### `ws.generate_prompt(instruction: string, selected_files: []string, show_all_files: bool) !string`
|
||||
|
||||
Generates a complete AI prompt with file map, contents, and instructions.
|
||||
|
||||
```v
|
||||
// Use selected files (from select_file() calls)
|
||||
prompt := ws.generate_prompt(
|
||||
instruction: 'Review the code'
|
||||
)!
|
||||
|
||||
// Or specify files explicitly
|
||||
prompt2 := ws.generate_prompt(
|
||||
instruction: 'Analyze these files'
|
||||
selected_files: ['/path/to/file1.v', '/path/to/file2.v']
|
||||
show_all_files: false
|
||||
)!
|
||||
```
|
||||
|
||||
#### `ws.generate_file_map(selected_files: []string, show_all: bool) !string`
|
||||
|
||||
Generates a hierarchical tree structure of files.
|
||||
|
||||
```v
|
||||
file_map := ws.generate_file_map(
|
||||
selected_files: ['/path/to/file1.v']
|
||||
show_all: false
|
||||
)!
|
||||
println(file_map)
|
||||
```
|
||||
|
||||
#### `ws.generate_file_contents(selected_files: []string, include_path: bool) !string`
|
||||
|
||||
Generates formatted file contents.
|
||||
|
||||
```v
|
||||
contents := ws.generate_file_contents(
|
||||
selected_files: ['/path/to/file1.v']
|
||||
include_path: true
|
||||
)!
|
||||
println(contents)
|
||||
```
|
||||
|
||||
## Directory Methods
|
||||
|
||||
### File Selection
|
||||
|
||||
#### `dir.select_file(path: string) !`
|
||||
|
||||
Marks a file as selected.
|
||||
|
||||
```v
|
||||
dir.select_file(path: '/path/to/file.v')!
|
||||
```
|
||||
|
||||
#### `dir.select_all() !`
|
||||
|
||||
Selects all files in the directory and subdirectories.
|
||||
|
||||
```v
|
||||
dir.select_all()!
|
||||
```
|
||||
|
||||
#### `dir.deselect_file(path: string) !`
|
||||
|
||||
Deselects a file.
|
||||
|
||||
```v
|
||||
dir.deselect_file(path: '/path/to/file.v')!
|
||||
```
|
||||
|
||||
#### `dir.deselect_all() !`
|
||||
|
||||
Deselects all files in the directory.
|
||||
|
||||
```v
|
||||
dir.deselect_all()!
|
||||
```
|
||||
|
||||
### Directory Information
|
||||
|
||||
#### `dir.exists() bool`
|
||||
|
||||
Checks if the directory exists on the filesystem.
|
||||
|
||||
```v
|
||||
if dir.exists() {
|
||||
println('Directory exists')
|
||||
}
|
||||
```
|
||||
|
||||
#### `dir.get_contents() !DirectoryContent`
|
||||
|
||||
Gets all files in the directory (scans if needed).
|
||||
|
||||
```v
|
||||
content := dir.get_contents()!
|
||||
println('Files: ${content.files.len}')
|
||||
```
|
||||
|
||||
## Generated Prompt Format
|
||||
|
||||
The generated prompt uses a template with three sections:
|
||||
|
||||
```prompt
|
||||
<user_instructions>
|
||||
Review these files and suggest improvements
|
||||
</user_instructions>
|
||||
|
||||
<file_map>
|
||||
my_project/
|
||||
├── src/
|
||||
│ ├── main.v *
|
||||
│ └── utils.v *
|
||||
└── README.md *
|
||||
</file_map>
|
||||
|
||||
<file_contents>
|
||||
File: /path/to/src/main.v
|
||||
\```v
|
||||
module main
|
||||
|
||||
fn main() {
|
||||
println('Hello')
|
||||
}
|
||||
\```
|
||||
|
||||
</file_contents>
|
||||
|
||||
```
|
||||
|
||||
Files marked with `*` in the file_map are the selected files included in the prompt.
|
||||
|
||||
## Complete Example
|
||||
|
||||
```v
|
||||
import incubaid.herolib.develop.heroprompt
|
||||
|
||||
mut hp := heroprompt.get(name: 'my_app', create: true)!
|
||||
mut ws := hp.new_workspace(name: 'backend')!
|
||||
|
||||
mut src_dir := ws.add_directory(path: '/path/to/src', name: 'source', scan: true)!
|
||||
src_dir.select_file(path: '/path/to/src/main.v')!
|
||||
|
||||
prompt := ws.generate_prompt(instruction: 'Review the code')!
|
||||
println(prompt)
|
||||
|
||||
heroprompt.delete(name: 'my_app')!
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `heroprompt.delete()` at start for fresh state
|
||||
- First workspace is automatically active
|
||||
- Changes auto-save to Redis
|
||||
- Use `scan: true` to discover all files
|
||||
- Create separate workspaces for different contexts
|
||||
198
examples/develop/heroprompt/README.md
Normal file
198
examples/develop/heroprompt/README.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# HeroPrompt Example
|
||||
|
||||
Generate structured AI prompts from your codebase with file selection and workspace management.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run the example:
|
||||
|
||||
```bash
|
||||
./examples/develop/heroprompt/prompt_example.vsh
|
||||
```
|
||||
|
||||
This example demonstrates the complete workflow from creating a workspace to generating AI prompts.
|
||||
|
||||
---
|
||||
|
||||
## What is HeroPrompt?
|
||||
|
||||
HeroPrompt helps you organize code files and generate structured prompts for AI analysis:
|
||||
|
||||
- **Workspace Management**: Organize files into logical workspaces
|
||||
- **File Selection**: Select specific files or entire directories
|
||||
- **Prompt Generation**: Generate formatted prompts with file trees and contents
|
||||
- **Redis Persistence**: All data persists across sessions
|
||||
- **Active Workspace**: Easily switch between different workspaces
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Create Instance and Workspace
|
||||
|
||||
```v
|
||||
import incubaid.herolib.develop.heroprompt
|
||||
|
||||
// Create or get instance
|
||||
mut hp := heroprompt.get(name: 'my_project', create: true)!
|
||||
|
||||
// Create workspace (first workspace is automatically active)
|
||||
mut workspace := hp.new_workspace(
|
||||
name: 'my_workspace'
|
||||
description: 'My project workspace'
|
||||
)!
|
||||
```
|
||||
|
||||
### 2. Add Directories
|
||||
|
||||
```v
|
||||
// Add directory and scan all files
|
||||
mut dir := workspace.add_directory(
|
||||
path: '/path/to/your/code'
|
||||
name: 'my_code'
|
||||
scan: true // Automatically scans all files and subdirectories
|
||||
)!
|
||||
```
|
||||
|
||||
### 3. Select Files
|
||||
|
||||
```v
|
||||
// Select specific files
|
||||
dir.select_file(path: '/path/to/file1.v')!
|
||||
dir.select_file(path: '/path/to/file2.v')!
|
||||
|
||||
// Or select all files in directory
|
||||
dir.select_all()!
|
||||
```
|
||||
|
||||
### 4. Generate Prompt
|
||||
|
||||
```v
|
||||
// Generate AI prompt with selected files
|
||||
prompt := workspace.generate_prompt(
|
||||
instruction: 'Review these files and suggest improvements'
|
||||
)!
|
||||
|
||||
println(prompt)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Generated Prompt Format
|
||||
|
||||
The generated prompt includes three sections:
|
||||
|
||||
```
|
||||
<user_instructions>
|
||||
Review these files and suggest improvements
|
||||
</user_instructions>
|
||||
|
||||
<file_map>
|
||||
my_project/
|
||||
├── src/
|
||||
│ ├── main.v *
|
||||
│ └── utils.v *
|
||||
└── README.md *
|
||||
</file_map>
|
||||
|
||||
<file_contents>
|
||||
File: /path/to/src/main.v
|
||||
```v
|
||||
module main
|
||||
...
|
||||
```
|
||||
|
||||
</file_contents>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Factory Functions
|
||||
|
||||
```v
|
||||
heroprompt.get(name: 'my_project', create: true)! // Get or create
|
||||
heroprompt.delete(name: 'my_project')! // Delete instance
|
||||
heroprompt.exists(name: 'my_project')! // Check if exists
|
||||
heroprompt.list()! // List all instances
|
||||
```
|
||||
|
||||
### HeroPrompt Methods
|
||||
|
||||
```v
|
||||
hp.new_workspace(name: 'ws', description: 'desc')! // Create workspace
|
||||
hp.get_workspace('ws')! // Get workspace by name
|
||||
hp.list_workspaces() // List all workspaces
|
||||
hp.delete_workspace('ws')! // Delete workspace
|
||||
hp.get_active_workspace()! // Get active workspace
|
||||
hp.set_active_workspace('ws')! // Set active workspace
|
||||
```
|
||||
|
||||
### Workspace Methods
|
||||
|
||||
```v
|
||||
ws.add_directory(path: '/path', name: 'dir', scan: true)! // Add directory
|
||||
ws.list_directories() // List directories
|
||||
ws.remove_directory(id: 'dir_id')! // Remove directory
|
||||
ws.generate_prompt(instruction: 'Review')! // Generate prompt
|
||||
ws.generate_file_map()! // Generate file tree
|
||||
ws.generate_file_contents()! // Generate contents
|
||||
```
|
||||
|
||||
### Directory Methods
|
||||
|
||||
```v
|
||||
dir.select_file(path: '/path/to/file')! // Select file
|
||||
dir.select_all()! // Select all files
|
||||
dir.deselect_file(path: '/path/to/file')! // Deselect file
|
||||
dir.deselect_all()! // Deselect all files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Active Workspace
|
||||
|
||||
```v
|
||||
// Get the currently active workspace
|
||||
mut active := hp.get_active_workspace()!
|
||||
|
||||
// Switch to a different workspace
|
||||
hp.set_active_workspace('other_workspace')!
|
||||
```
|
||||
|
||||
### Multiple Workspaces
|
||||
|
||||
```v
|
||||
// Create multiple workspaces for different purposes
|
||||
mut backend := hp.new_workspace(name: 'backend')!
|
||||
mut frontend := hp.new_workspace(name: 'frontend')!
|
||||
mut docs := hp.new_workspace(name: 'documentation')!
|
||||
```
|
||||
|
||||
### File Selection
|
||||
|
||||
```v
|
||||
// Select individual files
|
||||
dir.select_file(path: '/path/to/file.v')!
|
||||
|
||||
// Select all files in directory
|
||||
dir.select_all()!
|
||||
|
||||
// Deselect files
|
||||
dir.deselect_file(path: '/path/to/file.v')!
|
||||
dir.deselect_all()!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- Always start with cleanup (`heroprompt.delete()`) in examples to ensure a fresh state
|
||||
- The first workspace created is automatically set as active
|
||||
- File selection persists to Redis automatically
|
||||
- Use `scan: true` when adding directories to automatically scan all files
|
||||
- Selected files are tracked per directory for efficient management
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import incubaid.herolib.develop.heroprompt
|
||||
import os
|
||||
|
||||
// mut workspace := heroprompt.new(
|
||||
// path: '${os.home_dir()}/code/github/incubaid/herolib'
|
||||
// name: 'workspace'
|
||||
// )!
|
||||
|
||||
mut workspace := heroprompt.get(
|
||||
name: 'example_ws'
|
||||
path: '${os.home_dir()}/code/github/incubaid/herolib'
|
||||
create: true
|
||||
)!
|
||||
|
||||
println('workspace (initial): ${workspace}')
|
||||
println('selected (initial): ${workspace.selected_children()}')
|
||||
|
||||
// Add a directory and a file
|
||||
workspace.add_dir(path: '${os.home_dir()}/code/github/incubaid/herolib/docker')!
|
||||
workspace.add_file(
|
||||
path: '${os.home_dir()}/code/github/incubaid/herolib/docker/docker_ubuntu_install.sh'
|
||||
)!
|
||||
println('selected (after add): ${workspace.selected_children()}')
|
||||
|
||||
// Build a prompt from current selection (should be empty now)
|
||||
mut prompt := workspace.prompt(
|
||||
text: 'Using the selected files, i want you to get all print statments'
|
||||
)
|
||||
|
||||
println('--- PROMPT START ---')
|
||||
println(prompt)
|
||||
println('--- PROMPT END ---')
|
||||
|
||||
// Remove the file by name, then the directory by name
|
||||
workspace.remove_file(name: 'docker_ubuntu_install.sh') or { println('remove_file: ${err}') }
|
||||
workspace.remove_dir(name: 'docker') or { println('remove_dir: ${err}') }
|
||||
println('selected (after remove): ${workspace.selected_children()}')
|
||||
|
||||
// List workspaces (names only)
|
||||
mut all := heroprompt.list_workspaces() or { []&heroprompt.Workspace{} }
|
||||
mut names := []string{}
|
||||
for w in all {
|
||||
names << w.name
|
||||
}
|
||||
println('workspaces: ${names}')
|
||||
|
||||
// Optionally delete the example workspace
|
||||
workspace.delete_workspace() or { println('delete_workspace: ${err}') }
|
||||
145
examples/develop/heroprompt/prompt_example.vsh
Executable file
145
examples/develop/heroprompt/prompt_example.vsh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import incubaid.herolib.develop.heroprompt
|
||||
import os
|
||||
|
||||
println('=== HeroPrompt: AI Prompt Generation Example ===\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 1: Cleanup and Setup
|
||||
// ============================================================================
|
||||
// Always start fresh - delete any existing instance
|
||||
println('Step 1: Cleaning up any existing instance...')
|
||||
heroprompt.delete(name: 'prompt_demo') or {}
|
||||
println('✓ Cleanup complete\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 2: Create HeroPrompt Instance
|
||||
// ============================================================================
|
||||
// Get or create a new HeroPrompt instance
|
||||
// The 'create: true' parameter will create it if it doesn't exist
|
||||
println('Step 2: Creating HeroPrompt instance...')
|
||||
mut hp := heroprompt.get(name: 'prompt_demo', create: true)!
|
||||
println('✓ Created instance: ${hp.name}\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 3: Create Workspace
|
||||
// ============================================================================
|
||||
// A workspace is a collection of directories and files
|
||||
// The first workspace is automatically set as active
|
||||
println('Step 3: Creating workspace...')
|
||||
mut workspace := hp.new_workspace(
|
||||
name: 'my_project'
|
||||
description: 'Example project workspace'
|
||||
)!
|
||||
println('✓ Created workspace: ${workspace.name}')
|
||||
println(' Active: ${workspace.is_active}')
|
||||
println(' Description: ${workspace.description}\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 4: Add Directories to Workspace
|
||||
// ============================================================================
|
||||
// Add directories containing code you want to analyze
|
||||
// The 'scan: true' parameter automatically scans all files and subdirectories
|
||||
println('Step 4: Adding directories to workspace...')
|
||||
|
||||
homepath := os.home_dir()
|
||||
|
||||
// Add the examples directory
|
||||
mut examples_dir := workspace.add_directory(
|
||||
path: '${homepath}/code/github/incubaid/herolib/examples/develop/heroprompt'
|
||||
name: 'examples'
|
||||
scan: true
|
||||
)!
|
||||
println('✓ Added directory: examples')
|
||||
|
||||
// Add the library directory
|
||||
mut lib_dir := workspace.add_directory(
|
||||
path: '${homepath}/code/github/incubaid/herolib/lib/develop/heroprompt'
|
||||
name: 'library'
|
||||
scan: true
|
||||
)!
|
||||
println('✓ Added directory: library\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 5: Select Specific Files
|
||||
// ============================================================================
|
||||
// You can select specific files from directories for prompt generation
|
||||
// This is useful when you only want to analyze certain files
|
||||
println('Step 5: Selecting specific files...')
|
||||
|
||||
// Select individual files from the examples directory
|
||||
examples_dir.select_file(
|
||||
path: '${homepath}/code/github/incubaid/herolib/examples/develop/heroprompt/README.md'
|
||||
)!
|
||||
println('✓ Selected: README.md')
|
||||
|
||||
examples_dir.select_file(
|
||||
path: '${homepath}/code/github/incubaid/herolib/examples/develop/heroprompt/prompt_example.vsh'
|
||||
)!
|
||||
println('✓ Selected: prompt_example.vsh')
|
||||
|
||||
// Select all files from the library directory
|
||||
lib_dir.select_all()!
|
||||
println('✓ Selected all files in library directory\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 6: Generate AI Prompt
|
||||
// ============================================================================
|
||||
// Generate a complete prompt with file map, file contents, and instructions
|
||||
// The prompt automatically includes only the selected files
|
||||
println('Step 6: Generating AI prompt...')
|
||||
|
||||
prompt := workspace.generate_prompt(
|
||||
instruction: 'Review the selected files and provide suggestions for improvements.'
|
||||
)!
|
||||
|
||||
println('✓ Generated prompt')
|
||||
println(' Total length: ${prompt.len} characters\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 7: Display Prompt Preview
|
||||
// ============================================================================
|
||||
println('Step 7: Prompt preview (first 800 characters)...')
|
||||
preview_len := if prompt.len > 800 { 800 } else { prompt.len }
|
||||
println(prompt[..preview_len])
|
||||
|
||||
// ============================================================================
|
||||
// STEP 8: Alternative - Get Active Workspace
|
||||
// ============================================================================
|
||||
// You can retrieve the active workspace without knowing its name
|
||||
println('Step 8: Working with active workspace...')
|
||||
|
||||
mut active_ws := hp.get_active_workspace()!
|
||||
println('✓ Retrieved active workspace: ${active_ws.name}')
|
||||
println(' Directories: ${active_ws.directories.len}')
|
||||
println(' Files: ${active_ws.files.len}\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 9: Set Different Active Workspace
|
||||
// ============================================================================
|
||||
// You can create multiple workspaces and switch between them
|
||||
println('Step 9: Creating and switching workspaces...')
|
||||
|
||||
// Create a second workspace
|
||||
mut workspace2 := hp.new_workspace(
|
||||
name: 'documentation'
|
||||
description: 'Documentation workspace'
|
||||
is_active: false
|
||||
)!
|
||||
println('✓ Created workspace: ${workspace2.name}')
|
||||
|
||||
// Switch active workspace
|
||||
hp.set_active_workspace('documentation')!
|
||||
println('✓ Set active workspace to: documentation')
|
||||
|
||||
// Verify the switch
|
||||
active_ws = hp.get_active_workspace()!
|
||||
println('✓ Current active workspace: ${active_ws.name}\n')
|
||||
|
||||
// ============================================================================
|
||||
// STEP 10: Cleanup
|
||||
// ============================================================================
|
||||
println('Step 10: Cleanup...')
|
||||
heroprompt.delete(name: 'prompt_demo')!
|
||||
println('✓ Deleted instance\n')
|
||||
@@ -45,7 +45,7 @@ fn addtoscript(tofind string, toadd string) ! {
|
||||
// Reset symlinks (cleanup)
|
||||
println('Resetting all symlinks...')
|
||||
os.rm('${os.home_dir()}/.vmodules/incubaid/herolib') or {}
|
||||
os.rm('${os.home_dir()}/.vmodules/freeflowuniverse/herolib') or {}
|
||||
os.rm('${os.home_dir()}/.vmodules/incubaid/herolib') or {}
|
||||
|
||||
// Create necessary directories
|
||||
os.mkdir_all('${os.home_dir()}/.vmodules/incubaid') or {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
!!hero_code.generate_client
|
||||
name:'heroprompt'
|
||||
classname:'Workspace'
|
||||
singleton:0
|
||||
classname:'HeroPrompt'
|
||||
singleton:1
|
||||
default:1
|
||||
hasconfig:1
|
||||
templates:
|
||||
reset:0
|
||||
126
lib/develop/heroprompt/heroprompt.v
Normal file
126
lib/develop/heroprompt/heroprompt.v
Normal file
@@ -0,0 +1,126 @@
|
||||
module heroprompt
|
||||
|
||||
import rand
|
||||
import incubaid.herolib.data.ourtime
|
||||
|
||||
// HeroPrompt Methods - Workspace Management
|
||||
|
||||
@[params]
|
||||
pub struct NewWorkspaceParams {
|
||||
pub mut:
|
||||
name string @[required] // Workspace name
|
||||
description string // Optional description
|
||||
is_active bool = false // Whether this should be the active workspace
|
||||
}
|
||||
|
||||
// new_workspace creates a new workspace in this HeroPrompt instance
|
||||
pub fn (mut hp HeroPrompt) new_workspace(args NewWorkspaceParams) !&Workspace {
|
||||
hp.log(.info, 'Creating workspace: ${args.name}')
|
||||
|
||||
// Check if workspace already exists
|
||||
if args.name in hp.workspaces {
|
||||
hp.log(.error, 'Workspace already exists: ${args.name}')
|
||||
return error('workspace already exists: ${args.name}')
|
||||
}
|
||||
|
||||
// Determine if this should be the active workspace
|
||||
// If it's the first workspace, make it active by default
|
||||
// Or if explicitly requested via args.is_active
|
||||
is_first_workspace := hp.workspaces.len == 0
|
||||
should_be_active := args.is_active || is_first_workspace
|
||||
|
||||
// Create new workspace
|
||||
mut ws := &Workspace{
|
||||
id: rand.uuid_v4()
|
||||
name: args.name
|
||||
description: args.description
|
||||
is_active: should_be_active
|
||||
directories: map[string]&Directory{}
|
||||
files: []HeropromptFile{}
|
||||
created: ourtime.now()
|
||||
updated: ourtime.now()
|
||||
parent: &hp // Set parent reference for auto-save
|
||||
}
|
||||
|
||||
// Add to heroprompt instance
|
||||
hp.workspaces[args.name] = ws
|
||||
hp.updated = ourtime.now()
|
||||
|
||||
// Save to Redis
|
||||
hp.save()!
|
||||
|
||||
hp.log(.info, 'Workspace created: ${args.name}')
|
||||
return ws
|
||||
}
|
||||
|
||||
// get_workspace retrieves an existing workspace by name
|
||||
pub fn (hp &HeroPrompt) get_workspace(name string) !&Workspace {
|
||||
if name !in hp.workspaces {
|
||||
return error('workspace not found: ${name}')
|
||||
}
|
||||
return hp.workspaces[name]
|
||||
}
|
||||
|
||||
// list_workspaces returns all workspaces in this HeroPrompt instance
|
||||
pub fn (hp &HeroPrompt) list_workspaces() []&Workspace {
|
||||
mut workspaces := []&Workspace{}
|
||||
for _, ws in hp.workspaces {
|
||||
workspaces << ws
|
||||
}
|
||||
return workspaces
|
||||
}
|
||||
|
||||
// delete_workspace removes a workspace from this HeroPrompt instance
|
||||
pub fn (mut hp HeroPrompt) delete_workspace(name string) ! {
|
||||
if name !in hp.workspaces {
|
||||
hp.log(.error, 'Workspace not found: ${name}')
|
||||
return error('workspace not found: ${name}')
|
||||
}
|
||||
|
||||
hp.workspaces.delete(name)
|
||||
hp.updated = ourtime.now()
|
||||
hp.save()!
|
||||
|
||||
hp.log(.info, 'Workspace deleted: ${name}')
|
||||
}
|
||||
|
||||
// save persists the HeroPrompt instance to Redis
|
||||
pub fn (mut hp HeroPrompt) save() ! {
|
||||
hp.updated = ourtime.now()
|
||||
set(hp)!
|
||||
}
|
||||
|
||||
// get_active_workspace returns the currently active workspace
|
||||
pub fn (mut hp HeroPrompt) get_active_workspace() !&Workspace {
|
||||
for name, ws in hp.workspaces {
|
||||
if ws.is_active {
|
||||
// Return the actual reference from the map, not a copy
|
||||
return hp.workspaces[name] or { return error('workspace not found: ${name}') }
|
||||
}
|
||||
}
|
||||
return error('no active workspace found')
|
||||
}
|
||||
|
||||
// set_active_workspace sets the specified workspace as active and deactivates all others
|
||||
pub fn (mut hp HeroPrompt) set_active_workspace(name string) ! {
|
||||
// Check if workspace exists
|
||||
if name !in hp.workspaces {
|
||||
hp.log(.error, 'Workspace not found: ${name}')
|
||||
return error('workspace not found: ${name}')
|
||||
}
|
||||
|
||||
// Deactivate all workspaces
|
||||
for _, mut ws in hp.workspaces {
|
||||
ws.is_active = false
|
||||
}
|
||||
|
||||
// Activate the specified workspace
|
||||
mut ws := hp.workspaces[name] or { return error('workspace not found: ${name}') }
|
||||
ws.is_active = true
|
||||
hp.updated = ourtime.now()
|
||||
|
||||
// Save to Redis
|
||||
hp.save()!
|
||||
|
||||
hp.log(.info, 'Active workspace set to: ${name}')
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
module heroprompt
|
||||
|
||||
import incubaid.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
pub struct HeropromptChild {
|
||||
pub mut:
|
||||
content string
|
||||
path pathlib.Path
|
||||
name string
|
||||
include_tree bool // when true and this child is a dir, include full subtree in maps/contents
|
||||
}
|
||||
|
||||
// Utility function to get file extension with special handling for common files
|
||||
pub fn get_file_extension(filename string) string {
|
||||
// Handle special cases for common files without extensions
|
||||
special_files := {
|
||||
'dockerfile': 'dockerfile'
|
||||
'makefile': 'makefile'
|
||||
'license': 'license'
|
||||
'readme': 'readme'
|
||||
'changelog': 'changelog'
|
||||
'authors': 'authors'
|
||||
'contributors': 'contributors'
|
||||
'copying': 'copying'
|
||||
'install': 'install'
|
||||
'news': 'news'
|
||||
'todo': 'todo'
|
||||
'version': 'version'
|
||||
'manifest': 'manifest'
|
||||
'gemfile': 'gemfile'
|
||||
'rakefile': 'rakefile'
|
||||
'procfile': 'procfile'
|
||||
'vagrantfile': 'vagrantfile'
|
||||
}
|
||||
lower_filename := filename.to_lower()
|
||||
if lower_filename in special_files {
|
||||
return special_files[lower_filename]
|
||||
}
|
||||
if filename.starts_with('.') && !filename.starts_with('..') {
|
||||
if filename.contains('.') && filename.len > 1 {
|
||||
parts := filename[1..].split('.')
|
||||
if parts.len >= 2 {
|
||||
return parts[parts.len - 1]
|
||||
} else {
|
||||
return filename[1..]
|
||||
}
|
||||
} else {
|
||||
return filename[1..]
|
||||
}
|
||||
}
|
||||
parts := filename.split('.')
|
||||
if parts.len < 2 {
|
||||
return ''
|
||||
}
|
||||
return parts[parts.len - 1]
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
pub fn (chl HeropromptChild) read() !string {
|
||||
if chl.path.cat != .file {
|
||||
return error('cannot read content of a directory')
|
||||
}
|
||||
content := os.read_file(chl.path.path)!
|
||||
return content
|
||||
}
|
||||
467
lib/develop/heroprompt/heroprompt_directory.v
Normal file
467
lib/develop/heroprompt/heroprompt_directory.v
Normal file
@@ -0,0 +1,467 @@
|
||||
module heroprompt
|
||||
|
||||
import os
|
||||
import rand
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.develop.codewalker
|
||||
import incubaid.herolib.data.ourtime
|
||||
|
||||
// Directory represents a directory/directory added to a workspace
|
||||
// It contains metadata about the directory and its location
|
||||
@[heap]
|
||||
pub struct Directory {
|
||||
pub mut:
|
||||
id string = rand.uuid_v4() // Unique identifier for this directory
|
||||
name string // Display name (can be customized by user)
|
||||
path string // Absolute path to the directory
|
||||
description string // Optional description
|
||||
git_info GitInfo // Git directory information (if applicable)
|
||||
created ourtime.OurTime // When this directory was added
|
||||
updated ourtime.OurTime // Last update time
|
||||
include_tree bool = true // Whether to include full tree in file maps
|
||||
is_expanded bool // UI state: whether directory is expanded in tree view
|
||||
is_selected bool // UI state: whether directory checkbox is checked
|
||||
selected_files map[string]bool // Map of file paths to selection state (normalized paths)
|
||||
}
|
||||
|
||||
// GitInfo contains git-specific metadata for a directory
|
||||
pub struct GitInfo {
|
||||
pub mut:
|
||||
is_git_dir bool // Whether this is a git directory
|
||||
current_branch string // Current git branch
|
||||
remote_url string // Remote URL (if any)
|
||||
last_commit string // Last commit hash
|
||||
has_changes bool // Whether there are uncommitted changes
|
||||
}
|
||||
|
||||
// Create a new directory from a directory path
|
||||
@[params]
|
||||
pub struct NewDirectoryParams {
|
||||
pub mut:
|
||||
path string @[required] // Absolute path to directory
|
||||
name string // Optional custom name (defaults to directory name)
|
||||
description string // Optional description
|
||||
}
|
||||
|
||||
// Create a new directory instance
|
||||
pub fn new_directory(args NewDirectoryParams) !Directory {
|
||||
if args.path.len == 0 {
|
||||
return error('directory path is required')
|
||||
}
|
||||
|
||||
mut dir_path := pathlib.get(args.path)
|
||||
if !dir_path.exists() || !dir_path.is_dir() {
|
||||
return error('path is not an existing directory: ${args.path}')
|
||||
}
|
||||
|
||||
abs_path := dir_path.realpath()
|
||||
dir_name := dir_path.name()
|
||||
|
||||
// Detect git information
|
||||
git_info := detect_git_info(abs_path)
|
||||
|
||||
return Directory{
|
||||
id: rand.uuid_v4()
|
||||
name: if args.name.len > 0 { args.name } else { dir_name }
|
||||
path: abs_path
|
||||
description: args.description
|
||||
git_info: git_info
|
||||
created: ourtime.now()
|
||||
updated: ourtime.now()
|
||||
include_tree: true
|
||||
}
|
||||
}
|
||||
|
||||
// Detect git information for a directory
|
||||
fn detect_git_info(path string) GitInfo {
|
||||
// TODO: Use the gittools library to get this information
|
||||
// Keep it for now, maybe next version
|
||||
mut info := GitInfo{
|
||||
is_git_dir: false
|
||||
}
|
||||
|
||||
// Check if .git directory exists
|
||||
git_dir := os.join_path(path, '.git')
|
||||
if !os.exists(git_dir) {
|
||||
return info
|
||||
}
|
||||
|
||||
info.is_git_dir = true
|
||||
|
||||
// Try to detect current branch
|
||||
head_file := os.join_path(git_dir, 'HEAD')
|
||||
if os.exists(head_file) {
|
||||
head_content := os.read_file(head_file) or { '' }
|
||||
if head_content.contains('ref: refs/heads/') {
|
||||
info.current_branch = head_content.replace('ref: refs/heads/', '').trim_space()
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect remote URL
|
||||
config_file := os.join_path(git_dir, 'config')
|
||||
if os.exists(config_file) {
|
||||
config_content := os.read_file(config_file) or { '' }
|
||||
// Simple parsing - look for url = line
|
||||
for line in config_content.split_into_lines() {
|
||||
trimmed := line.trim_space()
|
||||
if trimmed.starts_with('url = ') {
|
||||
info.remote_url = trimmed.replace('url = ', '')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for uncommitted changes (simplified - just check if there are any files in git status)
|
||||
// In a real implementation, would run `git status --porcelain`
|
||||
info.has_changes = false // Placeholder - would need to execute git command
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// Update directory metadata
|
||||
@[params]
|
||||
pub struct UpdateDirectoryParams {
|
||||
pub mut:
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
pub fn (mut dir Directory) update(args UpdateDirectoryParams) {
|
||||
if args.name.len > 0 {
|
||||
dir.name = args.name
|
||||
}
|
||||
if args.description.len > 0 {
|
||||
dir.description = args.description
|
||||
}
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
// Refresh git information for this directory
|
||||
pub fn (mut dir Directory) refresh_git_info() {
|
||||
dir.git_info = detect_git_info(dir.path)
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
// Check if directory path still exists
|
||||
pub fn (dir &Directory) exists() bool {
|
||||
return os.exists(dir.path) && os.is_dir(dir.path)
|
||||
}
|
||||
|
||||
// Get directory size (number of files)
|
||||
pub fn (dir &Directory) file_count() !int {
|
||||
if !dir.exists() {
|
||||
return error('directory path no longer exists')
|
||||
}
|
||||
|
||||
// Use codewalker to count files
|
||||
mut cw := codewalker.new(codewalker.CodeWalkerArgs{})!
|
||||
mut fm := cw.filemap_get(path: dir.path, content_read: false)!
|
||||
return fm.content.len
|
||||
}
|
||||
|
||||
// Get display name with git branch if available
|
||||
pub fn (dir &Directory) display_name() string {
|
||||
if dir.git_info.is_git_dir && dir.git_info.current_branch.len > 0 {
|
||||
return '${dir.name} (${dir.git_info.current_branch})'
|
||||
}
|
||||
return dir.name
|
||||
}
|
||||
|
||||
// Directory Management Methods
|
||||
|
||||
// DirectoryContent holds the scanned files and directories from a directory
|
||||
pub struct DirectoryContent {
|
||||
pub mut:
|
||||
files []HeropromptFile // All files found in the directory
|
||||
directories []string // All directories found in the directory
|
||||
file_count int // Total number of files
|
||||
dir_count int // Total number of directories
|
||||
}
|
||||
|
||||
// get_contents scans the directory and returns all files and directories
|
||||
// This method respects .gitignore and .heroignore files
|
||||
// This is a public method that can be used to retrieve directory contents for prompt generation
|
||||
pub fn (dir &Directory) get_contents() !DirectoryContent {
|
||||
return dir.scan()
|
||||
}
|
||||
|
||||
// scan scans the entire directory and returns all files and directories
|
||||
// This method respects .gitignore and .heroignore files
|
||||
// Note: This is a private method. Use add_dir() with scan parameter or get_contents() instead.
|
||||
fn (dir &Directory) scan() !DirectoryContent {
|
||||
if !dir.exists() {
|
||||
return error('directory path does not exist: ${dir.path}')
|
||||
}
|
||||
|
||||
// Use codewalker to scan the directory with gitignore support
|
||||
mut cw := codewalker.new(codewalker.CodeWalkerArgs{})!
|
||||
mut fm := cw.filemap_get(path: dir.path, content_read: true)!
|
||||
|
||||
mut files := []HeropromptFile{}
|
||||
mut directories := map[string]bool{} // Use map to avoid duplicates
|
||||
|
||||
// Process each file from the filemap
|
||||
for file_path, content in fm.content {
|
||||
// Create HeropromptFile for each file
|
||||
abs_path := os.join_path(dir.path, file_path)
|
||||
file := HeropromptFile{
|
||||
id: rand.uuid_v4()
|
||||
name: os.base(file_path)
|
||||
path: abs_path
|
||||
content: content
|
||||
created: ourtime.now()
|
||||
updated: ourtime.now()
|
||||
}
|
||||
files << file
|
||||
|
||||
// Extract directory path
|
||||
dir_path := os.dir(file_path)
|
||||
if dir_path != '.' && dir_path.len > 0 {
|
||||
// Add all parent directories
|
||||
mut current_dir := dir_path
|
||||
for current_dir != '.' && current_dir.len > 0 {
|
||||
directories[current_dir] = true
|
||||
current_dir = os.dir(current_dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert directories map to array
|
||||
mut dir_list := []string{}
|
||||
for directory_path, _ in directories {
|
||||
dir_list << directory_path
|
||||
}
|
||||
|
||||
return DirectoryContent{
|
||||
files: files
|
||||
directories: dir_list
|
||||
file_count: files.len
|
||||
dir_count: dir_list.len
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct AddFileParams {
|
||||
pub mut:
|
||||
path string @[required] // Path to file (relative to directory or absolute)
|
||||
}
|
||||
|
||||
// add_file adds a specific file to the directory
|
||||
// Returns the created HeropromptFile
|
||||
pub fn (dir &Directory) add_file(args AddFileParams) !HeropromptFile {
|
||||
mut file_path := args.path
|
||||
|
||||
// If path is relative, make it relative to directory path
|
||||
if !os.is_abs_path(file_path) {
|
||||
file_path = os.join_path(dir.path, file_path)
|
||||
}
|
||||
|
||||
// Validate file exists
|
||||
if !os.exists(file_path) {
|
||||
return error('file does not exist: ${file_path}')
|
||||
}
|
||||
if os.is_dir(file_path) {
|
||||
return error('path is a directory, not a file: ${file_path}')
|
||||
}
|
||||
|
||||
// Read file content
|
||||
content := os.read_file(file_path) or { return error('failed to read file: ${file_path}') }
|
||||
|
||||
// Create HeropromptFile
|
||||
file := HeropromptFile{
|
||||
id: rand.uuid_v4()
|
||||
name: os.base(file_path)
|
||||
path: file_path
|
||||
content: content
|
||||
created: ourtime.now()
|
||||
updated: ourtime.now()
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct SelectFileParams {
|
||||
pub mut:
|
||||
path string @[required] // Path to file (relative to directory or absolute)
|
||||
}
|
||||
|
||||
// select_file marks a file as selected within this directory
|
||||
// The file path can be relative to the directory or absolute
|
||||
pub fn (mut dir Directory) select_file(args SelectFileParams) ! {
|
||||
// Normalize the path
|
||||
mut file_path := args.path
|
||||
if !os.is_abs_path(file_path) {
|
||||
file_path = os.join_path(dir.path, file_path)
|
||||
}
|
||||
file_path = os.real_path(file_path)
|
||||
|
||||
// Verify file exists
|
||||
if !os.exists(file_path) {
|
||||
return error('file does not exist: ${args.path}')
|
||||
}
|
||||
|
||||
// Verify file is within this directory
|
||||
if !file_path.starts_with(os.real_path(dir.path)) {
|
||||
return error('file is not within this directory: ${args.path}')
|
||||
}
|
||||
|
||||
// Mark file as selected
|
||||
dir.selected_files[file_path] = true
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
// select_all marks all files in this directory and subdirectories as selected
|
||||
pub fn (mut dir Directory) select_all() ! {
|
||||
// Verify directory exists
|
||||
if !dir.exists() {
|
||||
return error('directory does not exist: ${dir.path}')
|
||||
}
|
||||
|
||||
// Get all files in directory
|
||||
content := dir.get_contents()!
|
||||
|
||||
// Mark all files as selected
|
||||
for file in content.files {
|
||||
normalized_path := os.real_path(file.path)
|
||||
dir.selected_files[normalized_path] = true
|
||||
}
|
||||
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct DeselectFileParams {
|
||||
pub mut:
|
||||
path string @[required] // Path to file (relative to directory or absolute)
|
||||
}
|
||||
|
||||
// deselect_file marks a file as not selected within this directory
|
||||
pub fn (mut dir Directory) deselect_file(args DeselectFileParams) ! {
|
||||
// Normalize the path
|
||||
mut file_path := args.path
|
||||
if !os.is_abs_path(file_path) {
|
||||
file_path = os.join_path(dir.path, file_path)
|
||||
}
|
||||
file_path = os.real_path(file_path)
|
||||
|
||||
// Verify file exists
|
||||
if !os.exists(file_path) {
|
||||
return error('file does not exist: ${args.path}')
|
||||
}
|
||||
|
||||
// Verify file is within this directory
|
||||
if !file_path.starts_with(os.real_path(dir.path)) {
|
||||
return error('file is not within this directory: ${args.path}')
|
||||
}
|
||||
|
||||
// Mark file as not selected (remove from map or set to false)
|
||||
dir.selected_files.delete(file_path)
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
// deselect_all marks all files in this directory as not selected
|
||||
pub fn (mut dir Directory) deselect_all() ! {
|
||||
// Verify directory exists
|
||||
if !dir.exists() {
|
||||
return error('directory does not exist: ${dir.path}')
|
||||
}
|
||||
|
||||
// Clear all selections
|
||||
dir.selected_files.clear()
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
// expand sets the directory as expanded in the UI
|
||||
pub fn (mut dir Directory) expand() {
|
||||
dir.is_expanded = true
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
// collapse sets the directory as collapsed in the UI
|
||||
pub fn (mut dir Directory) collapse() {
|
||||
dir.is_expanded = false
|
||||
dir.updated = ourtime.now()
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct AddDirParams {
|
||||
pub mut:
|
||||
path string @[required] // Path to directory (relative to directory or absolute)
|
||||
scan bool = true // Whether to automatically scan the directory (default: true)
|
||||
}
|
||||
|
||||
// add_dir adds all files from a specific directory
|
||||
// Returns DirectoryContent with files from that directory
|
||||
// If scan=true (default), automatically scans the directory respecting .gitignore
|
||||
// If scan=false, returns empty DirectoryContent (manual mode)
|
||||
pub fn (dir &Directory) add_dir(args AddDirParams) !DirectoryContent {
|
||||
mut dir_path := args.path
|
||||
|
||||
// If path is relative, make it relative to directory path
|
||||
if !os.is_abs_path(dir_path) {
|
||||
dir_path = os.join_path(dir.path, dir_path)
|
||||
}
|
||||
|
||||
// Validate directory exists
|
||||
if !os.exists(dir_path) {
|
||||
return error('directory does not exist: ${dir_path}')
|
||||
}
|
||||
if !os.is_dir(dir_path) {
|
||||
return error('path is not a directory: ${dir_path}')
|
||||
}
|
||||
|
||||
// If scan is false, return empty content (manual mode)
|
||||
if !args.scan {
|
||||
return DirectoryContent{
|
||||
files: []HeropromptFile{}
|
||||
file_count: 0
|
||||
dir_count: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Use codewalker to scan the directory
|
||||
mut cw := codewalker.new(codewalker.CodeWalkerArgs{})!
|
||||
mut fm := cw.filemap_get(path: dir_path, content_read: true)!
|
||||
|
||||
mut files := []HeropromptFile{}
|
||||
mut directories := map[string]bool{} // Track directories
|
||||
|
||||
// Process each file
|
||||
for file_path, content in fm.content {
|
||||
abs_path := os.join_path(dir_path, file_path)
|
||||
file := HeropromptFile{
|
||||
id: rand.uuid_v4()
|
||||
name: os.base(file_path)
|
||||
path: abs_path
|
||||
content: content
|
||||
created: ourtime.now()
|
||||
updated: ourtime.now()
|
||||
}
|
||||
files << file
|
||||
|
||||
// Extract directory path
|
||||
dir_part := os.dir(file_path)
|
||||
if dir_part != '.' && dir_part.len > 0 {
|
||||
// Add all parent directories
|
||||
mut current_dir := dir_part
|
||||
for current_dir != '.' && current_dir.len > 0 {
|
||||
directories[current_dir] = true
|
||||
current_dir = os.dir(current_dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert directories map to array
|
||||
mut dir_list := []string{}
|
||||
for directory_path, _ in directories {
|
||||
dir_list << directory_path
|
||||
}
|
||||
|
||||
return DirectoryContent{
|
||||
files: files
|
||||
directories: dir_list
|
||||
file_count: files.len
|
||||
dir_count: dir_list.len
|
||||
}
|
||||
}
|
||||
288
lib/develop/heroprompt/heroprompt_directory_test.v
Normal file
288
lib/develop/heroprompt/heroprompt_directory_test.v
Normal file
@@ -0,0 +1,288 @@
|
||||
module heroprompt
|
||||
|
||||
import os
|
||||
|
||||
// Test directory: scan entire directory
|
||||
fn test_directory_scan() ! {
|
||||
// Create temp directory with files
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_scan')
|
||||
sub_dir := os.join_path(test_dir, 'src')
|
||||
os.mkdir_all(sub_dir)!
|
||||
os.write_file(os.join_path(test_dir, 'readme.md'), '# Test')!
|
||||
os.write_file(os.join_path(test_dir, 'main.v'), 'fn main() {}')!
|
||||
os.write_file(os.join_path(sub_dir, 'utils.v'), 'pub fn hello() {}')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Scan directory
|
||||
content := repo.scan()!
|
||||
|
||||
// Should find all files
|
||||
assert content.file_count >= 3
|
||||
assert content.files.len >= 3
|
||||
|
||||
// Should find directories
|
||||
assert content.dir_count >= 1
|
||||
|
||||
// Verify files have content
|
||||
mut found_readme := false
|
||||
for file in content.files {
|
||||
if file.name == 'readme.md' {
|
||||
found_readme = true
|
||||
assert file.content.contains('# Test')
|
||||
}
|
||||
}
|
||||
assert found_readme, 'readme.md not found in scanned files'
|
||||
}
|
||||
|
||||
// Test directory: scan respects gitignore
|
||||
fn test_directory_scan_gitignore() ! {
|
||||
// Create temp directory with .gitignore
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_gitignore')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
// Create .gitignore
|
||||
os.write_file(os.join_path(test_dir, '.gitignore'), 'ignored.txt\n*.log')!
|
||||
|
||||
// Create files
|
||||
os.write_file(os.join_path(test_dir, 'included.txt'), 'included')!
|
||||
os.write_file(os.join_path(test_dir, 'ignored.txt'), 'ignored')!
|
||||
os.write_file(os.join_path(test_dir, 'test.log'), 'log')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Scan directory
|
||||
content := repo.scan()!
|
||||
|
||||
// Should include included.txt
|
||||
mut found_included := false
|
||||
mut found_ignored := false
|
||||
mut found_log := false
|
||||
|
||||
for file in content.files {
|
||||
if file.name == 'included.txt' {
|
||||
found_included = true
|
||||
}
|
||||
if file.name == 'ignored.txt' {
|
||||
found_ignored = true
|
||||
}
|
||||
if file.name == 'test.log' {
|
||||
found_log = true
|
||||
}
|
||||
}
|
||||
|
||||
assert found_included, 'included.txt should be found'
|
||||
// Note: gitignore behavior depends on codewalker implementation
|
||||
// These assertions might need adjustment based on actual behavior
|
||||
}
|
||||
|
||||
// Test directory: add_file with relative path
|
||||
fn test_directory_add_file_relative() ! {
|
||||
// Create temp directory with file
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_add_file_rel')
|
||||
os.mkdir_all(test_dir)!
|
||||
os.write_file(os.join_path(test_dir, 'test.txt'), 'test content')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Add file with relative path
|
||||
file := repo.add_file(path: 'test.txt')!
|
||||
|
||||
assert file.name == 'test.txt'
|
||||
assert file.content == 'test content'
|
||||
assert file.path.contains('test.txt')
|
||||
}
|
||||
|
||||
// Test directory: add_file with absolute path
|
||||
fn test_directory_add_file_absolute() ! {
|
||||
// Create temp directory with file
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_add_file_abs')
|
||||
os.mkdir_all(test_dir)!
|
||||
test_file := os.join_path(test_dir, 'test.txt')
|
||||
os.write_file(test_file, 'test content')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Add file with absolute path
|
||||
file := repo.add_file(path: test_file)!
|
||||
|
||||
assert file.name == 'test.txt'
|
||||
assert file.content == 'test content'
|
||||
}
|
||||
|
||||
// Test directory: add_file non-existent file
|
||||
fn test_directory_add_file_nonexistent() ! {
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_add_nonexist')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Try to add non-existent file
|
||||
repo.add_file(path: 'nonexistent.txt') or {
|
||||
assert err.msg().contains('does not exist')
|
||||
return
|
||||
}
|
||||
|
||||
assert false, 'Expected error when adding non-existent file'
|
||||
}
|
||||
|
||||
// Test directory: add_dir with relative path
|
||||
fn test_directory_add_dir_relative() ! {
|
||||
// Create temp directory with subdirectory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_add_dir_rel')
|
||||
sub_dir := os.join_path(test_dir, 'src')
|
||||
os.mkdir_all(sub_dir)!
|
||||
os.write_file(os.join_path(sub_dir, 'file1.txt'), 'content1')!
|
||||
os.write_file(os.join_path(sub_dir, 'file2.txt'), 'content2')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Add directory with relative path
|
||||
content := repo.add_dir(path: 'src')!
|
||||
|
||||
assert content.file_count >= 2
|
||||
assert content.files.len >= 2
|
||||
}
|
||||
|
||||
// Test directory: add_dir with absolute path
|
||||
fn test_directory_add_dir_absolute() ! {
|
||||
// Create temp directory with subdirectory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_add_dir_abs')
|
||||
sub_dir := os.join_path(test_dir, 'src')
|
||||
os.mkdir_all(sub_dir)!
|
||||
os.write_file(os.join_path(sub_dir, 'file1.txt'), 'content1')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Add directory with absolute path
|
||||
content := repo.add_dir(path: sub_dir)!
|
||||
|
||||
assert content.file_count >= 1
|
||||
}
|
||||
|
||||
// Test directory: add_dir non-existent directory
|
||||
fn test_directory_add_dir_nonexistent() ! {
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_add_dir_nonexist')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Try to add non-existent directory
|
||||
repo.add_dir(path: 'nonexistent_dir') or {
|
||||
assert err.msg().contains('does not exist')
|
||||
return
|
||||
}
|
||||
|
||||
assert false, 'Expected error when adding non-existent directory'
|
||||
}
|
||||
|
||||
// Test directory: file_count
|
||||
fn test_directory_file_count() ! {
|
||||
// Create temp directory with files
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_file_count')
|
||||
os.mkdir_all(test_dir)!
|
||||
os.write_file(os.join_path(test_dir, 'file1.txt'), 'content1')!
|
||||
os.write_file(os.join_path(test_dir, 'file2.txt'), 'content2')!
|
||||
os.write_file(os.join_path(test_dir, 'file3.txt'), 'content3')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Get file count
|
||||
count := repo.file_count()!
|
||||
assert count >= 3
|
||||
}
|
||||
|
||||
// Test directory: display_name with git info
|
||||
fn test_directory_display_name() ! {
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_display_name')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
}
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir, name: 'Test Repo')!
|
||||
|
||||
// Display name should be the custom name
|
||||
display_name := repo.display_name()
|
||||
assert display_name == 'Test Repo'
|
||||
}
|
||||
|
||||
// Test directory: exists check
|
||||
fn test_directory_exists() ! {
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_exists')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
// Create directory
|
||||
repo := new_directory(path: test_dir)!
|
||||
|
||||
// Should exist
|
||||
assert repo.exists() == true
|
||||
|
||||
// Remove directory
|
||||
os.rmdir_all(test_dir)!
|
||||
|
||||
// Should not exist
|
||||
assert repo.exists() == false
|
||||
}
|
||||
@@ -2,11 +2,10 @@ module heroprompt
|
||||
|
||||
import incubaid.herolib.core.base
|
||||
import incubaid.herolib.core.playbook { PlayBook }
|
||||
import incubaid.herolib.ui.console
|
||||
import json
|
||||
|
||||
__global (
|
||||
heroprompt_global map[string]&Workspace
|
||||
heroprompt_global shared map[string]&HeroPrompt
|
||||
heroprompt_default string
|
||||
)
|
||||
|
||||
@@ -18,38 +17,68 @@ pub mut:
|
||||
name string = 'default'
|
||||
fromdb bool // will load from filesystem
|
||||
create bool // default will not create if not exist
|
||||
reset bool // will delete and recreate if exists
|
||||
}
|
||||
|
||||
pub fn new(args ArgsGet) !&Workspace {
|
||||
mut obj := Workspace{
|
||||
pub fn new(args ArgsGet) !&HeroPrompt {
|
||||
mut obj := HeroPrompt{
|
||||
name: args.name
|
||||
}
|
||||
set(obj)!
|
||||
return get(name: args.name)!
|
||||
}
|
||||
|
||||
pub fn get(args ArgsGet) !&Workspace {
|
||||
pub fn get(args ArgsGet) !&HeroPrompt {
|
||||
mut context := base.context()!
|
||||
heroprompt_default = args.name
|
||||
if args.fromdb || args.name !in heroprompt_global {
|
||||
mut r := context.redis()!
|
||||
mut r := context.redis()!
|
||||
|
||||
// Handle reset: delete existing instance and create fresh
|
||||
if args.reset {
|
||||
// Delete from Redis and memory
|
||||
r.hdel('context:heroprompt', args.name) or {}
|
||||
lock heroprompt_global {
|
||||
heroprompt_global.delete(args.name)
|
||||
}
|
||||
|
||||
// Create new instance
|
||||
mut obj := HeroPrompt{
|
||||
name: args.name
|
||||
}
|
||||
set(obj)!
|
||||
return get(name: args.name)! // Recursive call to load the new instance
|
||||
}
|
||||
|
||||
// Check if we need to load from DB
|
||||
needs_load := rlock heroprompt_global {
|
||||
args.fromdb || args.name !in heroprompt_global
|
||||
}
|
||||
|
||||
if needs_load {
|
||||
heroprompt_default = args.name
|
||||
|
||||
if r.hexists('context:heroprompt', args.name)! {
|
||||
// Load existing instance from Redis
|
||||
data := r.hget('context:heroprompt', args.name)!
|
||||
if data.len == 0 {
|
||||
print_backtrace()
|
||||
return error('Workspace with name: ${args.name} does not exist, prob bug.')
|
||||
}
|
||||
mut obj := json.decode(Workspace, data)!
|
||||
mut obj := json.decode(HeroPrompt, data)!
|
||||
set_in_mem(obj)!
|
||||
} else {
|
||||
// Instance doesn't exist in Redis
|
||||
if args.create {
|
||||
new(args)!
|
||||
// Create new instance
|
||||
mut obj := HeroPrompt{
|
||||
name: args.name
|
||||
}
|
||||
set(obj)!
|
||||
} else {
|
||||
print_backtrace()
|
||||
return error("Workspace with name '${args.name}' does not exist")
|
||||
}
|
||||
}
|
||||
return get(name: args.name)! // no longer from db nor create
|
||||
return get(name: args.name)! // Recursive call to return the instance
|
||||
}
|
||||
return heroprompt_global[args.name] or {
|
||||
print_backtrace()
|
||||
@@ -58,9 +87,10 @@ pub fn get(args ArgsGet) !&Workspace {
|
||||
}
|
||||
|
||||
// register the config for the future
|
||||
pub fn set(o Workspace) ! {
|
||||
pub fn set(o HeroPrompt) ! {
|
||||
mut o2 := set_in_mem(o)!
|
||||
heroprompt_default = o2.name
|
||||
|
||||
mut context := base.context()!
|
||||
mut r := context.redis()!
|
||||
r.hset('context:heroprompt', o2.name, json.encode(o2))!
|
||||
@@ -77,6 +107,11 @@ pub fn delete(args ArgsGet) ! {
|
||||
mut context := base.context()!
|
||||
mut r := context.redis()!
|
||||
r.hdel('context:heroprompt', args.name)!
|
||||
|
||||
// Also remove from memory
|
||||
lock heroprompt_global {
|
||||
heroprompt_global.delete(args.name)
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
@@ -86,15 +121,17 @@ pub mut:
|
||||
}
|
||||
|
||||
// if fromdb set: load from filesystem, and not from mem, will also reset what is in mem
|
||||
pub fn list(args ArgsList) ![]&Workspace {
|
||||
mut res := []&Workspace{}
|
||||
pub fn list(args ArgsList) ![]&HeroPrompt {
|
||||
mut res := []&HeroPrompt{}
|
||||
mut context := base.context()!
|
||||
|
||||
if args.fromdb {
|
||||
// reset what is in mem
|
||||
heroprompt_global = map[string]&Workspace{}
|
||||
lock heroprompt_global {
|
||||
heroprompt_global = map[string]&HeroPrompt{}
|
||||
}
|
||||
heroprompt_default = ''
|
||||
}
|
||||
if args.fromdb {
|
||||
|
||||
mut r := context.redis()!
|
||||
mut l := r.hkeys('context:heroprompt')!
|
||||
|
||||
@@ -104,18 +141,34 @@ pub fn list(args ArgsList) ![]&Workspace {
|
||||
return res
|
||||
} else {
|
||||
// load from memory
|
||||
for _, client in heroprompt_global {
|
||||
res << client
|
||||
rlock heroprompt_global {
|
||||
for _, client in heroprompt_global {
|
||||
res << client
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// only sets in mem, does not set as config
|
||||
fn set_in_mem(o Workspace) !Workspace {
|
||||
fn set_in_mem(o HeroPrompt) !HeroPrompt {
|
||||
mut o2 := obj_init(o)!
|
||||
heroprompt_global[o2.name] = &o2
|
||||
|
||||
// Restore parent references for all workspaces AFTER storing in global
|
||||
// This ensures the parent pointer points to the actual instance in memory
|
||||
lock heroprompt_global {
|
||||
heroprompt_global[o2.name] = &o2
|
||||
|
||||
// Now restore parent references using the stored instance
|
||||
mut stored := heroprompt_global[o2.name] or {
|
||||
return error('failed to store heroprompt instance in memory')
|
||||
}
|
||||
for _, mut ws in stored.workspaces {
|
||||
ws.parent = stored
|
||||
}
|
||||
}
|
||||
heroprompt_default = o2.name
|
||||
|
||||
return o2
|
||||
}
|
||||
|
||||
@@ -136,5 +189,4 @@ pub fn play(mut plbook PlayBook) ! {
|
||||
|
||||
// switch instance to be used for heroprompt
|
||||
pub fn switch(name string) {
|
||||
heroprompt_default = name
|
||||
}
|
||||
|
||||
118
lib/develop/heroprompt/heroprompt_file.v
Normal file
118
lib/develop/heroprompt/heroprompt_file.v
Normal file
@@ -0,0 +1,118 @@
|
||||
module heroprompt
|
||||
|
||||
import rand
|
||||
import incubaid.herolib.core.pathlib
|
||||
import incubaid.herolib.data.ourtime
|
||||
|
||||
// HeropromptFile represents a standalone file added to a workspace
|
||||
// (not part of a directory)
|
||||
@[heap]
|
||||
pub struct HeropromptFile {
|
||||
pub mut:
|
||||
id string = rand.uuid_v4() // Unique identifier
|
||||
name string // File name
|
||||
path string // Absolute path to file
|
||||
content string // File content (cached)
|
||||
created ourtime.OurTime // When added to workspace
|
||||
updated ourtime.OurTime // Last update time
|
||||
is_selected bool // UI state: whether file checkbox is checked
|
||||
}
|
||||
|
||||
// Create a new file instance
|
||||
@[params]
|
||||
pub struct NewFileParams {
|
||||
pub mut:
|
||||
path string @[required] // Absolute path to file
|
||||
}
|
||||
|
||||
pub fn new_file(args NewFileParams) !HeropromptFile {
|
||||
if args.path.len == 0 {
|
||||
return error('file path is required')
|
||||
}
|
||||
|
||||
mut file_path := pathlib.get(args.path)
|
||||
if !file_path.exists() || !file_path.is_file() {
|
||||
return error('path is not an existing file: ${args.path}')
|
||||
}
|
||||
|
||||
abs_path := file_path.realpath()
|
||||
file_name := file_path.name()
|
||||
|
||||
// Read file content
|
||||
content := file_path.read() or { '' }
|
||||
|
||||
return HeropromptFile{
|
||||
id: rand.uuid_v4()
|
||||
name: file_name
|
||||
path: abs_path
|
||||
content: content
|
||||
created: ourtime.now()
|
||||
updated: ourtime.now()
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh file content from disk
|
||||
pub fn (mut file HeropromptFile) refresh() ! {
|
||||
mut file_path := pathlib.get(file.path)
|
||||
if !file_path.exists() {
|
||||
return error('file no longer exists: ${file.path}')
|
||||
}
|
||||
file.content = file_path.read()!
|
||||
file.updated = ourtime.now()
|
||||
}
|
||||
|
||||
// Check if file still exists
|
||||
pub fn (file &HeropromptFile) exists() bool {
|
||||
mut file_path := pathlib.get(file.path)
|
||||
return file_path.exists() && file_path.is_file()
|
||||
}
|
||||
|
||||
// Get file extension
|
||||
pub fn (file &HeropromptFile) extension() string {
|
||||
return get_file_extension(file.name)
|
||||
}
|
||||
|
||||
// Utility function to get file extension with special handling for common files
|
||||
pub fn get_file_extension(filename string) string {
|
||||
// Handle special cases for common files without extensions
|
||||
special_files := {
|
||||
'dockerfile': 'dockerfile'
|
||||
'makefile': 'makefile'
|
||||
'license': 'license'
|
||||
'readme': 'readme'
|
||||
'changelog': 'changelog'
|
||||
'authors': 'authors'
|
||||
'contributors': 'contributors'
|
||||
'copying': 'copying'
|
||||
'install': 'install'
|
||||
'news': 'news'
|
||||
'todo': 'todo'
|
||||
'version': 'version'
|
||||
'manifest': 'manifest'
|
||||
'gemfile': 'gemfile'
|
||||
'rakefile': 'rakefile'
|
||||
'procfile': 'procfile'
|
||||
'vagrantfile': 'vagrantfile'
|
||||
}
|
||||
lower_filename := filename.to_lower()
|
||||
if lower_filename in special_files {
|
||||
return special_files[lower_filename]
|
||||
}
|
||||
if filename.starts_with('.') && !filename.starts_with('..') {
|
||||
if filename.contains('.') && filename.len > 1 {
|
||||
parts := filename[1..].split('.')
|
||||
if parts.len >= 2 {
|
||||
return parts[parts.len - 1]
|
||||
} else {
|
||||
return filename[1..]
|
||||
}
|
||||
} else {
|
||||
return filename[1..]
|
||||
}
|
||||
}
|
||||
parts := filename.split('.')
|
||||
if parts.len < 2 {
|
||||
return ''
|
||||
}
|
||||
return parts[parts.len - 1]
|
||||
}
|
||||
150
lib/develop/heroprompt/heroprompt_file_test.v
Normal file
150
lib/develop/heroprompt/heroprompt_file_test.v
Normal file
@@ -0,0 +1,150 @@
|
||||
module heroprompt
|
||||
|
||||
import os
|
||||
|
||||
// Test file: create new file
|
||||
fn test_new_file() ! {
|
||||
// Create temp file
|
||||
temp_base := os.temp_dir()
|
||||
test_file := os.join_path(temp_base, 'test_heroprompt_new_file.txt')
|
||||
os.write_file(test_file, 'test content')!
|
||||
|
||||
defer {
|
||||
os.rm(test_file) or {}
|
||||
}
|
||||
|
||||
// Create HeropromptFile
|
||||
file := new_file(path: test_file)!
|
||||
|
||||
assert file.name == 'test_heroprompt_new_file.txt'
|
||||
assert file.content == 'test content'
|
||||
assert file.path == os.real_path(test_file)
|
||||
assert file.id.len > 0
|
||||
}
|
||||
|
||||
// Test file: create file with non-existent path
|
||||
fn test_new_file_nonexistent() ! {
|
||||
// Try to create file with non-existent path
|
||||
new_file(path: '/nonexistent/path/file.txt') or {
|
||||
assert err.msg().contains('not an existing file')
|
||||
return
|
||||
}
|
||||
|
||||
assert false, 'Expected error when creating file with non-existent path'
|
||||
}
|
||||
|
||||
// Test file: refresh content
|
||||
fn test_file_refresh() ! {
|
||||
// Create temp file
|
||||
temp_base := os.temp_dir()
|
||||
test_file := os.join_path(temp_base, 'test_heroprompt_refresh.txt')
|
||||
os.write_file(test_file, 'initial content')!
|
||||
|
||||
defer {
|
||||
os.rm(test_file) or {}
|
||||
}
|
||||
|
||||
// Create HeropromptFile
|
||||
mut file := new_file(path: test_file)!
|
||||
assert file.content == 'initial content'
|
||||
|
||||
// Modify file on disk
|
||||
os.write_file(test_file, 'updated content')!
|
||||
|
||||
// Refresh file
|
||||
file.refresh()!
|
||||
assert file.content == 'updated content'
|
||||
}
|
||||
|
||||
// Test file: exists check
|
||||
fn test_file_exists() ! {
|
||||
// Create temp file
|
||||
temp_base := os.temp_dir()
|
||||
test_file := os.join_path(temp_base, 'test_heroprompt_file_exists.txt')
|
||||
os.write_file(test_file, 'content')!
|
||||
|
||||
// Create HeropromptFile
|
||||
file := new_file(path: test_file)!
|
||||
|
||||
// Should exist
|
||||
assert file.exists() == true
|
||||
|
||||
// Remove file
|
||||
os.rm(test_file)!
|
||||
|
||||
// Should not exist
|
||||
assert file.exists() == false
|
||||
}
|
||||
|
||||
// Test file: get extension
|
||||
fn test_file_extension() ! {
|
||||
// Create temp files with different extensions
|
||||
temp_base := os.temp_dir()
|
||||
|
||||
test_files := {
|
||||
'test.txt': 'txt'
|
||||
'test.v': 'v'
|
||||
'test.py': 'py'
|
||||
'archive.tar.gz': 'gz'
|
||||
'.gitignore': 'gitignore'
|
||||
'Dockerfile': 'dockerfile'
|
||||
'Makefile': 'makefile'
|
||||
'README': 'readme'
|
||||
}
|
||||
|
||||
for filename, expected_ext in test_files {
|
||||
test_file := os.join_path(temp_base, filename)
|
||||
os.write_file(test_file, 'content')!
|
||||
|
||||
file := new_file(path: test_file)!
|
||||
actual_ext := file.extension()
|
||||
|
||||
assert actual_ext == expected_ext, 'Expected ${expected_ext} for ${filename}, got ${actual_ext}'
|
||||
|
||||
os.rm(test_file)!
|
||||
}
|
||||
}
|
||||
|
||||
// Test get_file_extension utility function
|
||||
fn test_get_file_extension() ! {
|
||||
// Regular files
|
||||
assert get_file_extension('test.txt') == 'txt'
|
||||
assert get_file_extension('main.v') == 'v'
|
||||
assert get_file_extension('script.py') == 'py'
|
||||
|
||||
// Files with multiple dots
|
||||
assert get_file_extension('archive.tar.gz') == 'gz'
|
||||
assert get_file_extension('config.test.js') == 'js'
|
||||
|
||||
// Dotfiles
|
||||
assert get_file_extension('.gitignore') == 'gitignore'
|
||||
assert get_file_extension('.env') == 'env'
|
||||
|
||||
// Special files
|
||||
assert get_file_extension('Dockerfile') == 'dockerfile'
|
||||
assert get_file_extension('Makefile') == 'makefile'
|
||||
assert get_file_extension('README') == 'readme'
|
||||
assert get_file_extension('LICENSE') == 'license'
|
||||
|
||||
// Files without extension
|
||||
assert get_file_extension('noextension') == ''
|
||||
}
|
||||
|
||||
// Test file: generate UUIDs
|
||||
fn test_file_unique_ids() ! {
|
||||
// Create temp file
|
||||
temp_base := os.temp_dir()
|
||||
test_file := os.join_path(temp_base, 'test_heroprompt_unique_id.txt')
|
||||
os.write_file(test_file, 'content')!
|
||||
|
||||
defer {
|
||||
os.rm(test_file) or {}
|
||||
}
|
||||
|
||||
// Create HeropromptFile instance
|
||||
file1 := new_file(path: test_file)!
|
||||
|
||||
// ID should be a UUID (36 characters with dashes)
|
||||
assert file1.id.len == 36
|
||||
assert file1.id.contains('-')
|
||||
}
|
||||
48
lib/develop/heroprompt/heroprompt_logging.v
Normal file
48
lib/develop/heroprompt/heroprompt_logging.v
Normal file
@@ -0,0 +1,48 @@
|
||||
module heroprompt
|
||||
|
||||
import incubaid.herolib.ui.console
|
||||
import incubaid.herolib.core.logger
|
||||
|
||||
// log writes a log message with the specified level
|
||||
// Outputs to both console and log file (unless run_in_tests is true)
|
||||
pub fn (mut hp HeroPrompt) log(level LogLevel, text string) {
|
||||
// Skip logging if running in tests
|
||||
if hp.run_in_tests {
|
||||
return
|
||||
}
|
||||
|
||||
// Console output with appropriate colors
|
||||
match level {
|
||||
.error {
|
||||
console.print_stderr('ERROR: ${text}')
|
||||
}
|
||||
.warning {
|
||||
console.print_warn('WARNING: ${text}')
|
||||
}
|
||||
.info {
|
||||
console.print_info(text)
|
||||
}
|
||||
.debug {
|
||||
console.print_debug(text)
|
||||
}
|
||||
}
|
||||
|
||||
// File logging - use the stored logger instance (no resource leak)
|
||||
level_str := match level {
|
||||
.error { 'ERROR' }
|
||||
.warning { 'WARNING' }
|
||||
.info { 'INFO' }
|
||||
.debug { 'DEBUG' }
|
||||
}
|
||||
|
||||
logtype := match level {
|
||||
.error { logger.LogType.error }
|
||||
else { logger.LogType.stdout }
|
||||
}
|
||||
|
||||
hp.logger.log(
|
||||
cat: level_str
|
||||
log: text
|
||||
logtype: logtype
|
||||
) or { console.print_stderr('Failed to write log: ${err}') }
|
||||
}
|
||||
@@ -1,50 +1,94 @@
|
||||
module heroprompt
|
||||
|
||||
import time
|
||||
import incubaid.herolib.core.playbook
|
||||
import incubaid.herolib.data.ourtime
|
||||
import incubaid.herolib.data.encoderhero
|
||||
import incubaid.herolib.core.logger
|
||||
import rand
|
||||
|
||||
pub const version = '0.0.0'
|
||||
pub const version = '1.0.0'
|
||||
const singleton = false
|
||||
const default = true
|
||||
|
||||
// Workspace represents a workspace containing multiple directories
|
||||
// and their selected files for AI prompt generation
|
||||
// LogLevel represents the severity level of a log message
|
||||
pub enum LogLevel {
|
||||
error
|
||||
warning
|
||||
info
|
||||
debug
|
||||
}
|
||||
|
||||
// HeroPrompt is the main factory instance that manages workspaces
|
||||
@[heap]
|
||||
pub struct HeroPrompt {
|
||||
pub mut:
|
||||
id string = rand.uuid_v4() // Unique identifier
|
||||
name string = 'default' // Instance name
|
||||
workspaces map[string]&Workspace // Map of workspace name to workspace
|
||||
created ourtime.OurTime // Time of creation
|
||||
updated ourtime.OurTime // Time of last update
|
||||
log_path string // Path to log file
|
||||
run_in_tests bool // Flag to suppress logging during tests
|
||||
logger logger.Logger @[skip; str: skip] // Logger instance (reused, not serialized)
|
||||
}
|
||||
|
||||
// Workspace represents a collection of directories and files for prompt generation
|
||||
@[heap]
|
||||
pub struct Workspace {
|
||||
pub mut:
|
||||
name string = 'default' // Workspace name
|
||||
base_path string // Base path of the workspace
|
||||
children []HeropromptChild // List of directories and files in this workspace
|
||||
created time.Time // Time of creation
|
||||
updated time.Time // Time of last update
|
||||
is_saved bool
|
||||
id string // Unique identifier
|
||||
name string // Workspace name (required)
|
||||
description string // Workspace description (optional)
|
||||
is_active bool // Whether this is the active workspace
|
||||
directories map[string]&Directory // Map of directory ID to directory
|
||||
files []HeropromptFile // Standalone files in this workspace
|
||||
created ourtime.OurTime // Time of creation
|
||||
updated ourtime.OurTime // Time of last update
|
||||
parent ?&HeroPrompt @[skip; str: skip] // Reference to parent HeroPrompt (not serialized)
|
||||
}
|
||||
|
||||
// your checking & initialization code if needed
|
||||
fn obj_init(mycfg_ Workspace) !Workspace {
|
||||
return mycfg_
|
||||
// obj_init validates and initializes the HeroPrompt instance
|
||||
fn obj_init(mycfg_ HeroPrompt) !HeroPrompt {
|
||||
mut mycfg := mycfg_
|
||||
// Initialize workspaces map if nil
|
||||
if mycfg.workspaces.len == 0 {
|
||||
mycfg.workspaces = map[string]&Workspace{}
|
||||
}
|
||||
// Set ID if not set
|
||||
if mycfg.id.len == 0 {
|
||||
mycfg.id = rand.uuid_v4()
|
||||
}
|
||||
// Set timestamps if not set
|
||||
if mycfg.created.unixt == 0 {
|
||||
mycfg.created = ourtime.now()
|
||||
}
|
||||
if mycfg.updated.unixt == 0 {
|
||||
mycfg.updated = ourtime.now()
|
||||
}
|
||||
// Set default log path if not set
|
||||
if mycfg.log_path.len == 0 {
|
||||
mycfg.log_path = '/tmp/heroprompt_logs'
|
||||
}
|
||||
// Initialize logger instance (create directory if needed)
|
||||
mycfg.logger = logger.new(path: mycfg.log_path, console_output: false) or {
|
||||
// If logger creation fails, create a dummy logger to avoid nil reference
|
||||
// This can happen if path is invalid, but we'll handle it gracefully
|
||||
logger.new(path: '/tmp/heroprompt_logs', console_output: false) or {
|
||||
panic('Failed to initialize logger: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// Restore parent references for all workspaces
|
||||
// This is critical because parent references are not serialized to Redis
|
||||
for _, mut ws in mycfg.workspaces {
|
||||
ws.parent = &mycfg
|
||||
}
|
||||
|
||||
return mycfg
|
||||
}
|
||||
|
||||
/////////////NORMALLY NO NEED TO TOUCH
|
||||
|
||||
pub fn heroscript_loads(heroscript string) !Workspace {
|
||||
mut pb := playbook.new(text: heroscript)!
|
||||
// Accept either define or configure; prefer define if present
|
||||
mut action_name := 'heroprompt.define'
|
||||
if !pb.exists_once(filter: action_name) {
|
||||
action_name = 'heroprompt.configure'
|
||||
if !pb.exists_once(filter: action_name) {
|
||||
return error("heroprompt: missing 'heroprompt.define' or 'heroprompt.configure' action")
|
||||
}
|
||||
}
|
||||
mut action := pb.get(filter: action_name)!
|
||||
mut p := action.params
|
||||
|
||||
return Workspace{
|
||||
name: p.get_default('name', 'default')!
|
||||
base_path: p.get_default('base_path', '')!
|
||||
created: time.now()
|
||||
updated: time.now()
|
||||
children: []HeropromptChild{}
|
||||
}
|
||||
pub fn heroscript_loads(heroscript string) !HeroPrompt {
|
||||
mut obj := encoderhero.decode[HeroPrompt](heroscript)!
|
||||
return obj
|
||||
}
|
||||
|
||||
330
lib/develop/heroprompt/heroprompt_prompt.v
Normal file
330
lib/develop/heroprompt/heroprompt_prompt.v
Normal file
@@ -0,0 +1,330 @@
|
||||
module heroprompt
|
||||
|
||||
import incubaid.herolib.develop.codewalker
|
||||
import os
|
||||
|
||||
// Prompt generation functionality for HeroPrompt workspaces
|
||||
|
||||
// HeropromptTmpPrompt is the template struct for prompt generation
|
||||
struct HeropromptTmpPrompt {
|
||||
pub mut:
|
||||
user_instructions string
|
||||
file_map string
|
||||
file_contents string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GenerateFileMapParams {
|
||||
pub mut:
|
||||
selected_files []string // List of file paths to mark as selected (optional, if empty all files are selected)
|
||||
show_all bool // If true, show all files in directories; if false, show only selected files
|
||||
}
|
||||
|
||||
// generate_file_map generates a hierarchical tree structure of the workspace
|
||||
// with selected files marked with '*'
|
||||
pub fn (ws &Workspace) generate_file_map(args GenerateFileMapParams) !string {
|
||||
mut all_files := []string{}
|
||||
mut selected_set := map[string]bool{}
|
||||
|
||||
// Build set of selected files for quick lookup
|
||||
for path in args.selected_files {
|
||||
selected_set[path] = true
|
||||
}
|
||||
|
||||
// Collect all files from directories
|
||||
for _, dir in ws.directories {
|
||||
content := dir.get_contents() or { continue }
|
||||
for file in content.files {
|
||||
all_files << file.path
|
||||
}
|
||||
}
|
||||
|
||||
// Add standalone files
|
||||
for file in ws.files {
|
||||
all_files << file.path
|
||||
}
|
||||
|
||||
// If no specific files selected, select all
|
||||
mut files_to_show := if args.selected_files.len > 0 {
|
||||
args.selected_files.clone()
|
||||
} else {
|
||||
all_files.clone()
|
||||
}
|
||||
|
||||
// Find common base path
|
||||
mut base_path := ''
|
||||
if files_to_show.len > 0 {
|
||||
base_path = find_common_base_path(files_to_show)
|
||||
}
|
||||
|
||||
// Generate tree using codewalker
|
||||
mut tree := ''
|
||||
if args.show_all {
|
||||
// Show full directory tree with selected files marked
|
||||
tree = generate_full_tree_with_selection(files_to_show, all_files, base_path)
|
||||
} else {
|
||||
// Show minimal tree with only selected files
|
||||
tree = codewalker.build_file_tree_selected(files_to_show, base_path)
|
||||
}
|
||||
|
||||
// Add config note
|
||||
mut output := 'Config: directory-only view; selected files shown.\n\n'
|
||||
|
||||
// Add base path if available
|
||||
if base_path.len > 0 {
|
||||
output += '${os.base(base_path)}\n'
|
||||
}
|
||||
|
||||
output += tree
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// find_common_base_path finds the common base directory for a list of file paths
|
||||
fn find_common_base_path(paths []string) string {
|
||||
if paths.len == 0 {
|
||||
return ''
|
||||
}
|
||||
|
||||
if paths.len == 1 {
|
||||
return os.dir(paths[0])
|
||||
}
|
||||
|
||||
// Split all paths into components
|
||||
mut path_parts := [][]string{}
|
||||
for path in paths {
|
||||
parts := path.split(os.path_separator)
|
||||
path_parts << parts
|
||||
}
|
||||
|
||||
// Find common prefix
|
||||
mut common := []string{}
|
||||
|
||||
// Find minimum length
|
||||
mut min_len := path_parts[0].len
|
||||
for parts in path_parts {
|
||||
if parts.len < min_len {
|
||||
min_len = parts.len
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0 .. min_len - 1 { // -1 to exclude filename
|
||||
part := path_parts[0][i]
|
||||
mut all_match := true
|
||||
for j in 1 .. path_parts.len {
|
||||
if path_parts[j][i] != part {
|
||||
all_match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if all_match {
|
||||
common << part
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if common.len == 0 {
|
||||
return ''
|
||||
}
|
||||
|
||||
return common.join(os.path_separator)
|
||||
}
|
||||
|
||||
// generate_full_tree_with_selection generates a full directory tree with selected files marked
|
||||
fn generate_full_tree_with_selection(selected_files []string, all_files []string, base_path string) string {
|
||||
// For now, use the minimal tree approach
|
||||
// TODO: Implement full tree with selective marking
|
||||
return codewalker.build_file_tree_selected(selected_files, base_path)
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GenerateFileContentsParams {
|
||||
pub mut:
|
||||
selected_files []string // List of file paths to include (optional, if empty all files are included)
|
||||
include_path bool = true // Include file path as header
|
||||
}
|
||||
|
||||
// generate_file_contents generates formatted file contents section
|
||||
pub fn (ws &Workspace) generate_file_contents(args GenerateFileContentsParams) !string {
|
||||
mut output := ''
|
||||
mut files_to_include := map[string]HeropromptFile{}
|
||||
|
||||
// Collect all files from directories
|
||||
for _, dir in ws.directories {
|
||||
content := dir.get_contents() or { continue }
|
||||
for file in content.files {
|
||||
// Normalize path for consistent comparison
|
||||
normalized_path := os.real_path(file.path)
|
||||
|
||||
// Create a mutable copy to update selection state
|
||||
mut file_copy := file
|
||||
|
||||
// Check if file is selected in directory's selection map
|
||||
if normalized_path in dir.selected_files {
|
||||
file_copy.is_selected = dir.selected_files[normalized_path]
|
||||
}
|
||||
|
||||
files_to_include[normalized_path] = file_copy
|
||||
}
|
||||
}
|
||||
|
||||
// Add standalone files
|
||||
for file in ws.files {
|
||||
// Normalize path for consistent comparison
|
||||
normalized_path := os.real_path(file.path)
|
||||
files_to_include[normalized_path] = file
|
||||
}
|
||||
|
||||
// Filter by selected files if specified
|
||||
mut files_to_output := []HeropromptFile{}
|
||||
if args.selected_files.len > 0 {
|
||||
for path in args.selected_files {
|
||||
// Normalize the selected path for comparison
|
||||
normalized_path := os.real_path(path)
|
||||
if normalized_path in files_to_include {
|
||||
files_to_output << files_to_include[normalized_path]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, file in files_to_include {
|
||||
files_to_output << file
|
||||
}
|
||||
}
|
||||
|
||||
// Sort files by path for consistent output
|
||||
files_to_output.sort(a.path < b.path)
|
||||
|
||||
// Generate content for each file
|
||||
for file in files_to_output {
|
||||
if args.include_path {
|
||||
output += 'File: ${file.path}\n'
|
||||
}
|
||||
|
||||
// Determine language for syntax highlighting
|
||||
ext := file.extension()
|
||||
lang := if ext.len > 0 { ext } else { 'text' }
|
||||
|
||||
output += '```${lang}\n'
|
||||
output += file.content
|
||||
if !file.content.ends_with('\n') {
|
||||
output += '\n'
|
||||
}
|
||||
output += '```\n\n'
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GeneratePromptParams {
|
||||
pub mut:
|
||||
instruction string // User's instruction/question
|
||||
selected_files []string // List of file paths to include (optional, if empty all files are included)
|
||||
show_all_files bool // If true, show all files in file_map; if false, show only selected
|
||||
}
|
||||
|
||||
// generate_prompt generates a complete AI prompt combining file_map, file_contents, and user instructions
|
||||
// If selected_files is empty, automatically uses files marked as selected in the workspace
|
||||
pub fn (ws &Workspace) generate_prompt(args GeneratePromptParams) !string {
|
||||
// Determine which files to include
|
||||
mut files_to_include := args.selected_files.clone()
|
||||
|
||||
// If no files specified, use selected files from workspace
|
||||
if files_to_include.len == 0 {
|
||||
files_to_include = ws.get_selected_files() or { []string{} }
|
||||
|
||||
// If still no files, return error with helpful message
|
||||
if files_to_include.len == 0 {
|
||||
return error('no files selected for prompt generation. Use select_file() to select files or provide selected_files parameter')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate file map
|
||||
file_map := ws.generate_file_map(
|
||||
selected_files: files_to_include
|
||||
show_all: args.show_all_files
|
||||
)!
|
||||
|
||||
// Generate file contents
|
||||
file_contents := ws.generate_file_contents(
|
||||
selected_files: files_to_include
|
||||
include_path: true
|
||||
)!
|
||||
|
||||
// Build user instructions
|
||||
mut user_instructions := args.instruction
|
||||
if user_instructions.len > 0 && !user_instructions.ends_with('\n') {
|
||||
user_instructions += '\n'
|
||||
}
|
||||
|
||||
// Use template to generate prompt
|
||||
prompt := HeropromptTmpPrompt{
|
||||
user_instructions: user_instructions
|
||||
file_map: file_map
|
||||
file_contents: file_contents
|
||||
}
|
||||
|
||||
result := $tmpl('./templates/prompt.template')
|
||||
return result
|
||||
}
|
||||
|
||||
// generate_prompt_simple is a convenience method that generates a prompt with just an instruction
|
||||
// and includes all files from the workspace
|
||||
pub fn (ws &Workspace) generate_prompt_simple(instruction string) !string {
|
||||
return ws.generate_prompt(
|
||||
instruction: instruction
|
||||
selected_files: []
|
||||
show_all_files: false
|
||||
)
|
||||
}
|
||||
|
||||
// get_all_file_paths returns all file paths in the workspace
|
||||
pub fn (ws &Workspace) get_all_file_paths() ![]string {
|
||||
mut paths := []string{}
|
||||
|
||||
// Collect from directories
|
||||
for _, dir in ws.directories {
|
||||
content := dir.get_contents() or { continue }
|
||||
for file in content.files {
|
||||
paths << file.path
|
||||
}
|
||||
}
|
||||
|
||||
// Add standalone files
|
||||
for file in ws.files {
|
||||
paths << file.path
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// filter_files_by_extension filters file paths by extension
|
||||
pub fn (ws &Workspace) filter_files_by_extension(extensions []string) ![]string {
|
||||
all_paths := ws.get_all_file_paths()!
|
||||
mut filtered := []string{}
|
||||
|
||||
for path in all_paths {
|
||||
ext := os.file_ext(path).trim_left('.')
|
||||
if ext in extensions {
|
||||
filtered << path
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// filter_files_by_pattern filters file paths by pattern (simple substring match)
|
||||
pub fn (ws &Workspace) filter_files_by_pattern(pattern string) ![]string {
|
||||
all_paths := ws.get_all_file_paths()!
|
||||
mut filtered := []string{}
|
||||
|
||||
pattern_lower := pattern.to_lower()
|
||||
for path in all_paths {
|
||||
if path.to_lower().contains(pattern_lower) {
|
||||
filtered << path
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
192
lib/develop/heroprompt/heroprompt_test.v
Normal file
192
lib/develop/heroprompt/heroprompt_test.v
Normal file
@@ -0,0 +1,192 @@
|
||||
module heroprompt
|
||||
|
||||
import incubaid.herolib.core.base
|
||||
|
||||
// Test HeroPrompt: new_workspace
|
||||
fn test_heroprompt_new_workspace() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_new_ws_hp') or {}
|
||||
|
||||
defer {
|
||||
delete(name: 'test_new_ws_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance
|
||||
mut hp := get(name: 'test_new_ws_hp', create: true)!
|
||||
hp.run_in_tests = true
|
||||
|
||||
// Create workspace
|
||||
ws := hp.new_workspace(name: 'test_workspace', description: 'Test workspace')!
|
||||
|
||||
assert ws.name == 'test_workspace'
|
||||
assert ws.description == 'Test workspace'
|
||||
assert ws.id.len == 36 // UUID length
|
||||
assert hp.workspaces.len == 1
|
||||
}
|
||||
|
||||
// Test HeroPrompt: get_workspace
|
||||
fn test_heroprompt_get_workspace() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_get_ws_hp') or {}
|
||||
|
||||
defer {
|
||||
delete(name: 'test_get_ws_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_get_ws_hp', create: true)!
|
||||
hp.run_in_tests = true
|
||||
hp.new_workspace(name: 'test_workspace')!
|
||||
|
||||
// Get workspace
|
||||
ws := hp.get_workspace('test_workspace')!
|
||||
assert ws.name == 'test_workspace'
|
||||
|
||||
// Try to get non-existent workspace
|
||||
hp.get_workspace('nonexistent') or {
|
||||
assert err.msg().contains('workspace not found')
|
||||
return
|
||||
}
|
||||
assert false, 'Expected error when getting non-existent workspace'
|
||||
}
|
||||
|
||||
// Test HeroPrompt: list_workspaces
|
||||
fn test_heroprompt_list_workspaces() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_list_ws_hp') or {}
|
||||
|
||||
defer {
|
||||
delete(name: 'test_list_ws_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance
|
||||
mut hp := get(name: 'test_list_ws_hp', create: true)!
|
||||
hp.run_in_tests = true
|
||||
|
||||
// Initially empty
|
||||
assert hp.list_workspaces().len == 0
|
||||
|
||||
// Create workspaces
|
||||
hp.new_workspace(name: 'workspace1')!
|
||||
hp.new_workspace(name: 'workspace2')!
|
||||
hp.new_workspace(name: 'workspace3')!
|
||||
|
||||
// List workspaces
|
||||
workspaces := hp.list_workspaces()
|
||||
assert workspaces.len == 3
|
||||
}
|
||||
|
||||
// Test HeroPrompt: delete_workspace
|
||||
fn test_heroprompt_delete_workspace() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_del_ws_hp') or {}
|
||||
|
||||
defer {
|
||||
delete(name: 'test_del_ws_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_del_ws_hp', create: true)!
|
||||
hp.run_in_tests = true
|
||||
hp.new_workspace(name: 'test_workspace')!
|
||||
|
||||
assert hp.workspaces.len == 1
|
||||
|
||||
// Delete workspace
|
||||
hp.delete_workspace('test_workspace')!
|
||||
assert hp.workspaces.len == 0
|
||||
|
||||
// Try to delete non-existent workspace
|
||||
hp.delete_workspace('nonexistent') or {
|
||||
assert err.msg().contains('workspace not found')
|
||||
return
|
||||
}
|
||||
assert false, 'Expected error when deleting non-existent workspace'
|
||||
}
|
||||
|
||||
// Test HeroPrompt: duplicate workspace
|
||||
fn test_heroprompt_duplicate_workspace() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_dup_ws_hp') or {}
|
||||
|
||||
defer {
|
||||
delete(name: 'test_dup_ws_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_dup_ws_hp', create: true)!
|
||||
hp.run_in_tests = true
|
||||
hp.new_workspace(name: 'test_workspace')!
|
||||
|
||||
// Try to create duplicate workspace
|
||||
hp.new_workspace(name: 'test_workspace') or {
|
||||
assert err.msg().contains('workspace already exists')
|
||||
return
|
||||
}
|
||||
assert false, 'Expected error when creating duplicate workspace'
|
||||
}
|
||||
|
||||
// Test HeroPrompt: auto-save after workspace operations
|
||||
fn test_heroprompt_auto_save() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_autosave_hp') or {}
|
||||
|
||||
defer {
|
||||
delete(name: 'test_autosave_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_autosave_hp', create: true)!
|
||||
hp.run_in_tests = true
|
||||
mut ws := hp.new_workspace(name: 'test_workspace')!
|
||||
|
||||
// Get fresh instance from Redis to verify save
|
||||
hp2 := get(name: 'test_autosave_hp', fromdb: true)!
|
||||
assert hp2.workspaces.len == 1
|
||||
assert 'test_workspace' in hp2.workspaces
|
||||
}
|
||||
|
||||
// Test HeroPrompt: logging suppression in tests
|
||||
fn test_heroprompt_logging_suppression() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_logging_hp') or {}
|
||||
|
||||
defer {
|
||||
delete(name: 'test_logging_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance
|
||||
mut hp := get(name: 'test_logging_hp', create: true)!
|
||||
|
||||
// Test with logging enabled (should not crash)
|
||||
hp.run_in_tests = false
|
||||
hp.log(.info, 'Test log message')
|
||||
|
||||
// Test with logging disabled
|
||||
hp.run_in_tests = true
|
||||
hp.log(.info, 'This should be suppressed')
|
||||
|
||||
// If we get here without crashing, logging works
|
||||
assert true
|
||||
}
|
||||
302
lib/develop/heroprompt/heroprompt_workspace_test.v
Normal file
302
lib/develop/heroprompt/heroprompt_workspace_test.v
Normal file
@@ -0,0 +1,302 @@
|
||||
module heroprompt
|
||||
|
||||
import os
|
||||
import incubaid.herolib.core.base
|
||||
|
||||
// Test workspace: add directory
|
||||
fn test_workspace_add_directory() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_add_repo_hp') or {}
|
||||
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_add_repo')
|
||||
os.mkdir_all(test_dir)!
|
||||
os.write_file(os.join_path(test_dir, 'test.txt'), 'test content')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
delete(name: 'test_add_repo_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_add_repo_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add directory
|
||||
repo := ws.add_directory(path: test_dir, name: 'Test Repo')!
|
||||
|
||||
assert ws.directories.len == 1
|
||||
assert repo.name == 'Test Repo'
|
||||
assert repo.path == os.real_path(test_dir)
|
||||
assert repo.id.len > 0
|
||||
}
|
||||
|
||||
// Test workspace: add directory without custom name
|
||||
fn test_workspace_add_directory_auto_name() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_auto_name_hp') or {}
|
||||
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'my_custom_dir_name')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
delete(name: 'test_auto_name_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_auto_name_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add directory without custom name
|
||||
repo := ws.add_directory(path: test_dir)!
|
||||
|
||||
// Name should be extracted from directory name
|
||||
assert repo.name == 'my_custom_dir_name'
|
||||
}
|
||||
|
||||
// Test workspace: add duplicate directory
|
||||
fn test_workspace_add_duplicate_directory() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_dup_repo_hp') or {}
|
||||
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_dup_repo')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
delete(name: 'test_dup_repo_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_dup_repo_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add directory
|
||||
ws.add_directory(path: test_dir)!
|
||||
|
||||
// Try to add same directory again
|
||||
ws.add_directory(path: test_dir) or {
|
||||
assert err.msg().contains('already added')
|
||||
return
|
||||
}
|
||||
|
||||
assert false, 'Expected error when adding duplicate directory'
|
||||
}
|
||||
|
||||
// Test workspace: remove directory by ID
|
||||
fn test_workspace_remove_directory_by_id() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_remove_repo_hp') or {}
|
||||
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_remove_repo')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
delete(name: 'test_remove_repo_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_remove_repo_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add directory
|
||||
repo := ws.add_directory(path: test_dir)!
|
||||
assert ws.directories.len == 1
|
||||
|
||||
// Remove by ID
|
||||
ws.remove_directory(id: repo.id)!
|
||||
assert ws.directories.len == 0
|
||||
}
|
||||
|
||||
// Test workspace: remove directory by path
|
||||
fn test_workspace_remove_directory_by_path() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_remove_path_hp') or {}
|
||||
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_remove_path')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
delete(name: 'test_remove_path_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_remove_path_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add directory
|
||||
ws.add_directory(path: test_dir)!
|
||||
assert ws.directories.len == 1
|
||||
|
||||
// Remove by path
|
||||
ws.remove_directory(path: test_dir)!
|
||||
assert ws.directories.len == 0
|
||||
}
|
||||
|
||||
// Test workspace: get directory
|
||||
fn test_workspace_get_directory() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_get_repo_hp') or {}
|
||||
|
||||
// Create temp directory
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_get_repo')
|
||||
os.mkdir_all(test_dir)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
delete(name: 'test_get_repo_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_get_repo_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add directory
|
||||
repo := ws.add_directory(path: test_dir)!
|
||||
|
||||
// Get directory
|
||||
retrieved_repo := ws.get_directory(repo.id)!
|
||||
assert retrieved_repo.id == repo.id
|
||||
assert retrieved_repo.path == repo.path
|
||||
}
|
||||
|
||||
// Test workspace: list directories
|
||||
fn test_workspace_list_directories() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_list_repos_hp') or {}
|
||||
|
||||
// Create temp directories
|
||||
temp_base := os.temp_dir()
|
||||
test_dir1 := os.join_path(temp_base, 'test_heroprompt_list_1')
|
||||
test_dir2 := os.join_path(temp_base, 'test_heroprompt_list_2')
|
||||
os.mkdir_all(test_dir1)!
|
||||
os.mkdir_all(test_dir2)!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir1) or {}
|
||||
os.rmdir_all(test_dir2) or {}
|
||||
delete(name: 'test_list_repos_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_list_repos_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add directories
|
||||
ws.add_directory(path: test_dir1)!
|
||||
ws.add_directory(path: test_dir2)!
|
||||
|
||||
// List directories
|
||||
repos := ws.list_directories()
|
||||
assert repos.len == 2
|
||||
}
|
||||
|
||||
// Test workspace: add file
|
||||
fn test_workspace_add_file() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_add_file_hp') or {}
|
||||
|
||||
// Create temp file
|
||||
temp_base := os.temp_dir()
|
||||
test_file := os.join_path(temp_base, 'test_heroprompt_add_file.txt')
|
||||
os.write_file(test_file, 'test file content')!
|
||||
|
||||
defer {
|
||||
os.rm(test_file) or {}
|
||||
delete(name: 'test_add_file_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_add_file_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Add file
|
||||
file := ws.add_file(path: test_file)!
|
||||
|
||||
assert ws.files.len == 1
|
||||
assert file.content == 'test file content'
|
||||
assert file.path == os.real_path(test_file)
|
||||
}
|
||||
|
||||
// Test workspace: item count
|
||||
fn test_workspace_item_count() ! {
|
||||
mut ctx := base.context()!
|
||||
mut r := ctx.redis()!
|
||||
|
||||
// Clean up
|
||||
r.hdel('context:heroprompt', 'test_count_hp') or {}
|
||||
|
||||
// Create temp directory and file
|
||||
temp_base := os.temp_dir()
|
||||
test_dir := os.join_path(temp_base, 'test_heroprompt_count_dir')
|
||||
test_file := os.join_path(temp_base, 'test_heroprompt_count_file.txt')
|
||||
os.mkdir_all(test_dir)!
|
||||
os.write_file(test_file, 'content')!
|
||||
|
||||
defer {
|
||||
os.rmdir_all(test_dir) or {}
|
||||
os.rm(test_file) or {}
|
||||
delete(name: 'test_count_hp') or {}
|
||||
}
|
||||
|
||||
// Create heroprompt instance and workspace
|
||||
mut hp := get(name: 'test_count_hp', create: true)!
|
||||
hp.run_in_tests = true // Suppress logging during tests
|
||||
mut ws := hp.new_workspace(name: 'test_ws')!
|
||||
|
||||
// Initially empty
|
||||
assert ws.item_count() == 0
|
||||
|
||||
// Add directory
|
||||
ws.add_directory(path: test_dir)!
|
||||
assert ws.item_count() == 1
|
||||
|
||||
// Add file
|
||||
ws.add_file(path: test_file)!
|
||||
assert ws.item_count() == 2
|
||||
}
|
||||
@@ -1,28 +1,167 @@
|
||||
# heroprompt
|
||||
# Heroprompt Module
|
||||
|
||||
To get started
|
||||
A hierarchical workspace-based system for organizing code files and directories for AI code analysis. The module follows a clean hierarchical API structure: HeroPrompt → Workspace → Directory.
|
||||
|
||||
## Features
|
||||
|
||||
- **Hierarchical API**: Clean three-level structure (HeroPrompt → Workspace → Directory)
|
||||
- **Workspace Management**: Create and manage multiple workspaces within a HeroPrompt instance
|
||||
- **Directory Support**: Add entire directories with automatic file discovery
|
||||
- **Flexible Scanning**: Support both automatic scanning (with gitignore) and manual file selection
|
||||
- **Optional Naming**: Directory names are optional - automatically extracted from directory names
|
||||
- **GitIgnore Support**: Automatic respect for `.gitignore` and `.heroignore` files during scanning
|
||||
- **Redis Storage**: Persistent storage using Redis with automatic save on mutations
|
||||
- **Centralized Logging**: Dual output to console and log file with configurable log levels
|
||||
- **Concurrency Safe**: Thread-safe global state management with proper locking
|
||||
|
||||
## Hierarchical API Structure
|
||||
|
||||
```v
|
||||
|
||||
|
||||
import incubaid.herolib.develop.heroprompt
|
||||
|
||||
// Example Usage:
|
||||
// 1. Get or create a heroprompt instance
|
||||
mut hp := heroprompt.get(name: 'my_heroprompt', create: true)!
|
||||
|
||||
// 1. Create a new workspace
|
||||
mut workspace := heroprompt.new(name: 'my_workspace', path: os.getwd())!
|
||||
// 2. Create workspaces from the heroprompt instance
|
||||
mut workspace1 := hp.new_workspace(name: 'workspace1', description: 'First workspace')!
|
||||
mut workspace2 := hp.new_workspace(name: 'workspace2', description: 'Second workspace')!
|
||||
|
||||
// 2. Add a directory to the workspace
|
||||
workspace.add_dir(path: './my_project_dir')!
|
||||
|
||||
// 3. Add a file to the workspace
|
||||
workspace.add_file(path: './my_project_dir/main.v')!
|
||||
|
||||
// 4. Generate a prompt
|
||||
user_instructions := 'Explain the code in main.v'
|
||||
prompt_output := workspace.prompt(text: user_instructions)
|
||||
println(prompt_output)
|
||||
// 3. Add directories to workspaces
|
||||
// Name parameter is optional - if not provided, extracts from path (last directory name)
|
||||
dir1 := workspace1.add_directory(
|
||||
path: '/path/to/directory'
|
||||
name: 'optional_custom_name' // Optional: defaults to last part of path
|
||||
)!
|
||||
|
||||
// 4. Directory operations - Mode A: Automatic scanning (default)
|
||||
// add_dir() automatically scans the directory respecting .gitignore
|
||||
content := dir1.add_dir(path: 'src')! // Automatically scans by default
|
||||
println('Scanned ${content.file_count} files, ${content.dir_count} directories')
|
||||
|
||||
// 5. Directory operations - Mode B: Manual selective addition
|
||||
dir2 := workspace1.add_directory(path: '/path/to/dir2')!
|
||||
file := dir2.add_file(path: 'specific_file.v')! // Add specific file
|
||||
dir_content := dir2.add_dir(path: 'src', scan: false)! // Manual mode (no auto-scan)
|
||||
|
||||
// Note: Workspace operations (add_directory, remove_directory, add_file, remove_file)
|
||||
// automatically save to Redis. No manual hp.save() call needed!
|
||||
```
|
||||
|
||||
## API Overview
|
||||
|
||||
### HeroPrompt Factory Functions
|
||||
|
||||
- `get(name, fromdb, create, reset)` - Get or create a HeroPrompt instance (main entry point)
|
||||
- `name: string` - Instance name (default: 'default')
|
||||
- `fromdb: bool` - Force reload from Redis (default: false, uses cache)
|
||||
- `create: bool` - Create if doesn't exist (default: false, returns error if not found)
|
||||
- `reset: bool` - Delete and recreate if exists (default: false)
|
||||
- `exists(name)` - Check if HeroPrompt instance exists
|
||||
- `delete(name)` - Delete a HeroPrompt instance
|
||||
- `list(fromdb)` - List all HeroPrompt instances
|
||||
|
||||
**Usage Examples:**
|
||||
|
||||
```v
|
||||
// Get existing instance (error if not found)
|
||||
hp := heroprompt.get(name: 'my_heroprompt')!
|
||||
|
||||
// Get or create instance
|
||||
hp := heroprompt.get(name: 'my_heroprompt', create: true)!
|
||||
|
||||
// Force reload from Redis (bypass cache)
|
||||
hp := heroprompt.get(name: 'my_heroprompt', fromdb: true)!
|
||||
|
||||
// Reset instance (delete and recreate)
|
||||
hp := heroprompt.get(name: 'my_heroprompt', reset: true)!
|
||||
```
|
||||
|
||||
### HeroPrompt Methods
|
||||
|
||||
- `new_workspace(name, description)` - Create a new workspace
|
||||
- `get_workspace(name)` - Get an existing workspace
|
||||
- `list_workspaces()` - List all workspaces
|
||||
- `delete_workspace(name)` - Delete a workspace
|
||||
- `save()` - Save HeroPrompt instance to Redis
|
||||
|
||||
### Workspace Methods
|
||||
|
||||
- `add_directory(path, name, description)` - Add a directory (name is optional)
|
||||
- `remove_directory(id/path/name)` - Remove a directory
|
||||
- `get_directory(id)` - Get a directory by ID
|
||||
- `list_directories()` - List all directories
|
||||
- `add_file(path)` - Add a standalone file
|
||||
- `remove_file(id/path/name)` - Remove a file
|
||||
- `get_file(id)` - Get a file by ID
|
||||
- `list_files()` - List all standalone files
|
||||
- `item_count()` - Get total number of items (directories + files)
|
||||
|
||||
### Directory Methods
|
||||
|
||||
- `add_file(path)` - Add a specific file (relative or absolute path)
|
||||
- `add_dir(path, scan: bool = true)` - Add all files from a specific directory
|
||||
- `scan: true` (default) - Automatically scans the directory respecting .gitignore
|
||||
- `scan: false` - Manual mode, returns empty content (for selective file addition)
|
||||
- `file_count()` - Get number of files in directory
|
||||
- `display_name()` - Get display name (includes git branch if available)
|
||||
- `exists()` - Check if directory path still exists
|
||||
- `refresh_git_info()` - Refresh git metadata
|
||||
|
||||
**Note:** The `scan()` method is now private and called automatically by `add_dir()` when `scan=true`.
|
||||
|
||||
## Testing
|
||||
|
||||
Run all tests:
|
||||
|
||||
```bash
|
||||
v -enable-globals -no-skip-unused -stats test lib/develop/heroprompt
|
||||
```
|
||||
|
||||
Run specific test files:
|
||||
|
||||
```bash
|
||||
v -enable-globals -no-skip-unused test lib/develop/heroprompt/heroprompt_workspace_test.v
|
||||
v -enable-globals -no-skip-unused test lib/develop/heroprompt/heroprompt_directory_test.v
|
||||
v -enable-globals -no-skip-unused test lib/develop/heroprompt/heroprompt_file_test.v
|
||||
```
|
||||
|
||||
## Logging Configuration
|
||||
|
||||
The module includes centralized logging with dual output (console + file):
|
||||
|
||||
```v
|
||||
mut hp := heroprompt.new(name: 'my_heroprompt', create: true)!
|
||||
|
||||
// Configure log path (default: '/tmp/heroprompt_logs')
|
||||
hp.log_path = '/custom/log/path'
|
||||
|
||||
// Suppress logging during tests
|
||||
hp.run_in_tests = true
|
||||
```
|
||||
|
||||
Log levels: `.error`, `.warning`, `.info`, `.debug`
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### v1.0.0
|
||||
|
||||
- **Repository → Directory**: The `Repository` struct and all related methods have been renamed to `Directory` to better reflect their purpose
|
||||
- `add_repository()` → `add_directory()`
|
||||
- `remove_repository()` → `remove_directory()`
|
||||
- `get_repository()` → `get_directory()`
|
||||
- `list_repositories()` → `list_directories()`
|
||||
- **Auto-save**: Workspace mutation methods now automatically save to Redis. Manual `hp.save()` calls are no longer required after workspace operations
|
||||
- **Time Fields**: All time fields now use `ourtime.OurTime` instead of `time.Time`
|
||||
- **UUID IDs**: All entities now use UUID v4 for unique identifiers instead of custom hash-based IDs
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/develop/heroprompt/` directory for comprehensive examples:
|
||||
|
||||
- **01_heroprompt_create_example.vsh** - Creating HeroPrompt instances with workspaces and directories
|
||||
- **02_heroprompt_get_example.vsh** - Retrieving and working with existing instances
|
||||
- **README.md** - Detailed guide on running the examples
|
||||
|
||||
Run the examples in order (create first, then get) to see the full workflow.
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
|
||||
<file_contents>
|
||||
@{prompt.file_contents}
|
||||
</file_contents>
|
||||
</file_contents>
|
||||
|
||||
@@ -18,6 +18,7 @@ pub fn build_selected_tree(files []string, base_root string) string {
|
||||
}
|
||||
rels << rp
|
||||
}
|
||||
|
||||
rels.sort()
|
||||
return tree_from_rel_paths(rels, '')
|
||||
}
|
||||
@@ -84,6 +85,13 @@ pub:
|
||||
|
||||
// build_file_tree_fs builds a file system tree for given root directories
|
||||
pub fn build_file_tree_fs(roots []string, prefix string) string {
|
||||
// Create ignore matcher with default patterns
|
||||
ignore_matcher := gitignore_matcher_new()
|
||||
return build_file_tree_fs_with_ignore(roots, prefix, &ignore_matcher)
|
||||
}
|
||||
|
||||
// build_file_tree_fs_with_ignore builds a file system tree with ignore pattern filtering
|
||||
pub fn build_file_tree_fs_with_ignore(roots []string, prefix string, ignore_matcher &IgnoreMatcher) string {
|
||||
mut out := ''
|
||||
for i, root in roots {
|
||||
if !os.is_dir(root) {
|
||||
@@ -92,41 +100,91 @@ pub fn build_file_tree_fs(roots []string, prefix string) string {
|
||||
connector := if i == roots.len - 1 { '└── ' } else { '├── ' }
|
||||
out += '${prefix}${connector}${os.base(root)}\n'
|
||||
child_prefix := if i == roots.len - 1 { prefix + ' ' } else { prefix + '│ ' }
|
||||
// list children under root
|
||||
entries := os.ls(root) or { []string{} }
|
||||
// sort: dirs first then files
|
||||
mut dirs := []string{}
|
||||
mut files := []string{}
|
||||
for e in entries {
|
||||
fp := os.join_path(root, e)
|
||||
if os.is_dir(fp) {
|
||||
dirs << fp
|
||||
} else if os.is_file(fp) {
|
||||
files << fp
|
||||
}
|
||||
out += build_file_tree_fs_recursive_with_ignore(root, child_prefix, '', ignore_matcher)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// build_file_tree_fs_recursive builds the contents of a directory without adding the directory name itself
|
||||
fn build_file_tree_fs_recursive(root string, prefix string) string {
|
||||
// Create ignore matcher with default patterns for backward compatibility
|
||||
ignore_matcher := gitignore_matcher_new()
|
||||
return build_file_tree_fs_recursive_with_ignore(root, prefix, '', &ignore_matcher)
|
||||
}
|
||||
|
||||
// build_file_tree_fs_recursive_with_ignore builds the contents of a directory with ignore pattern filtering
|
||||
fn build_file_tree_fs_recursive_with_ignore(root string, prefix string, base_rel_path string, ignore_matcher &IgnoreMatcher) string {
|
||||
mut out := ''
|
||||
// list children under root
|
||||
entries := os.ls(root) or { []string{} }
|
||||
// sort: dirs first then files
|
||||
mut dirs := []string{}
|
||||
mut files := []string{}
|
||||
|
||||
for e in entries {
|
||||
fp := os.join_path(root, e)
|
||||
|
||||
// Calculate relative path for ignore checking
|
||||
rel_path := if base_rel_path.len > 0 {
|
||||
if base_rel_path.ends_with('/') { base_rel_path + e } else { base_rel_path + '/' + e }
|
||||
} else {
|
||||
e
|
||||
}
|
||||
dirs.sort()
|
||||
files.sort()
|
||||
// files
|
||||
for j, f in files {
|
||||
file_connector := if j == files.len - 1 && dirs.len == 0 {
|
||||
'└── '
|
||||
|
||||
// Check if this entry should be ignored
|
||||
mut should_ignore := ignore_matcher.is_ignored(rel_path)
|
||||
if os.is_dir(fp) && !should_ignore {
|
||||
// Also check directory pattern with trailing slash
|
||||
should_ignore = ignore_matcher.is_ignored(rel_path + '/')
|
||||
}
|
||||
|
||||
if should_ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
if os.is_dir(fp) {
|
||||
dirs << fp
|
||||
} else if os.is_file(fp) {
|
||||
files << fp
|
||||
}
|
||||
}
|
||||
|
||||
dirs.sort()
|
||||
files.sort()
|
||||
|
||||
// files
|
||||
for j, f in files {
|
||||
file_connector := if j == files.len - 1 && dirs.len == 0 {
|
||||
'└── '
|
||||
} else {
|
||||
'├── '
|
||||
}
|
||||
out += '${prefix}${file_connector}${os.base(f)} *\n'
|
||||
}
|
||||
|
||||
// subdirectories
|
||||
for j, d in dirs {
|
||||
sub_connector := if j == dirs.len - 1 { '└── ' } else { '├── ' }
|
||||
out += '${prefix}${sub_connector}${os.base(d)}\n'
|
||||
sub_prefix := if j == dirs.len - 1 {
|
||||
prefix + ' '
|
||||
} else {
|
||||
prefix + '│ '
|
||||
}
|
||||
|
||||
// Calculate new relative path for subdirectory
|
||||
dir_name := os.base(d)
|
||||
new_rel_path := if base_rel_path.len > 0 {
|
||||
if base_rel_path.ends_with('/') {
|
||||
base_rel_path + dir_name
|
||||
} else {
|
||||
'├── '
|
||||
base_rel_path + '/' + dir_name
|
||||
}
|
||||
out += '${child_prefix}${file_connector}${os.base(f)} *\n'
|
||||
}
|
||||
// subdirectories
|
||||
for j, d in dirs {
|
||||
sub_connector := if j == dirs.len - 1 { '└── ' } else { '├── ' }
|
||||
out += '${child_prefix}${sub_connector}${os.base(d)}\n'
|
||||
sub_prefix := if j == dirs.len - 1 {
|
||||
child_prefix + ' '
|
||||
} else {
|
||||
child_prefix + '│ '
|
||||
}
|
||||
out += build_file_tree_fs([d], sub_prefix)
|
||||
} else {
|
||||
dir_name
|
||||
}
|
||||
|
||||
out += build_file_tree_fs_recursive_with_ignore(d, sub_prefix, new_rel_path, ignore_matcher)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -149,3 +207,72 @@ pub fn build_file_tree_selected(files []string, base_root string) string {
|
||||
rels.sort()
|
||||
return tree_from_rel_paths(rels, '')
|
||||
}
|
||||
|
||||
// SearchResult represents a search result item
|
||||
pub struct SearchResult {
|
||||
pub:
|
||||
name string // filename
|
||||
path string // relative path from base
|
||||
full_path string // absolute path
|
||||
typ string // 'file' or 'directory'
|
||||
}
|
||||
|
||||
// search_files searches for files and directories matching a query string
|
||||
// - base_path: the root directory to search from
|
||||
// - query: the search query (case-insensitive substring match)
|
||||
// - ignore_matcher: optional ignore matcher to filter results (can be null)
|
||||
// Returns a list of SearchResult items
|
||||
pub fn search_files(base_path string, query string, ignore_matcher &IgnoreMatcher) ![]SearchResult {
|
||||
if query.len == 0 {
|
||||
return []SearchResult{}
|
||||
}
|
||||
|
||||
query_lower := query.to_lower()
|
||||
mut results := []SearchResult{}
|
||||
|
||||
search_directory_recursive(base_path, base_path, query_lower, ignore_matcher, mut
|
||||
results)!
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// search_directory_recursive recursively searches directories for matching files
|
||||
fn search_directory_recursive(dir_path string, base_path string, query_lower string, ignore_matcher &IgnoreMatcher, mut results []SearchResult) ! {
|
||||
entries := os.ls(dir_path) or { return }
|
||||
|
||||
for entry in entries {
|
||||
full_path := os.join_path(dir_path, entry)
|
||||
|
||||
// Calculate relative path from base_path
|
||||
mut rel_path := full_path
|
||||
if full_path.starts_with(base_path) {
|
||||
rel_path = full_path[base_path.len..]
|
||||
if rel_path.starts_with('/') {
|
||||
rel_path = rel_path[1..]
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this entry should be ignored
|
||||
if unsafe { ignore_matcher != 0 } {
|
||||
if ignore_matcher.is_ignored(rel_path) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check if filename or path matches search query
|
||||
if entry.to_lower().contains(query_lower) || rel_path.to_lower().contains(query_lower) {
|
||||
results << SearchResult{
|
||||
name: entry
|
||||
path: rel_path
|
||||
full_path: full_path
|
||||
typ: if os.is_dir(full_path) { 'directory' } else { 'file' }
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively search subdirectories
|
||||
if os.is_dir(full_path) {
|
||||
search_directory_recursive(full_path, base_path, query_lower, ignore_matcher, mut
|
||||
results)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,5 +110,13 @@ pub fn print_info(txt string) {
|
||||
c.reset()
|
||||
}
|
||||
|
||||
// import incubaid.herolib.ui.console
|
||||
// console.print_header()
|
||||
// Print warning in yellow color
|
||||
pub fn print_warn(txt string) {
|
||||
mut c := get()
|
||||
if c.prev_title || c.prev_item {
|
||||
lf()
|
||||
}
|
||||
txt2 := trim(texttools.indent(txt, ' . '))
|
||||
cprintln(foreground: .yellow, text: txt2)
|
||||
c.reset()
|
||||
}
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
module ui
|
||||
|
||||
import veb
|
||||
import os
|
||||
import json
|
||||
import incubaid.herolib.develop.heroprompt as hp
|
||||
|
||||
// Types
|
||||
struct DirResp {
|
||||
path string
|
||||
items []hp.ListItem
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
fn expand_home_path(path string) string {
|
||||
if path.starts_with('~') {
|
||||
home := os.home_dir()
|
||||
return os.join_path(home, path.all_after('~'))
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
fn json_error(message string) string {
|
||||
return '{"error":"${message}"}'
|
||||
}
|
||||
|
||||
fn json_success() string {
|
||||
return '{"ok":true}'
|
||||
}
|
||||
|
||||
// Recursive search function
|
||||
fn search_directory(dir_path string, base_path string, query_lower string, mut results []map[string]string) {
|
||||
entries := os.ls(dir_path) or { return }
|
||||
|
||||
for entry in entries {
|
||||
full_path := os.join_path(dir_path, entry)
|
||||
|
||||
// Skip hidden files and common ignore patterns
|
||||
if entry.starts_with('.') || entry == 'node_modules' || entry == 'target'
|
||||
|| entry == 'build' {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get relative path from workspace base
|
||||
mut rel_path := full_path
|
||||
if full_path.starts_with(base_path) {
|
||||
rel_path = full_path[base_path.len..]
|
||||
if rel_path.starts_with('/') {
|
||||
rel_path = rel_path[1..]
|
||||
}
|
||||
}
|
||||
|
||||
// Check if filename or path matches search query
|
||||
if entry.to_lower().contains(query_lower) || rel_path.to_lower().contains(query_lower) {
|
||||
results << {
|
||||
'name': entry
|
||||
'path': rel_path
|
||||
'full_path': full_path
|
||||
'type': if os.is_dir(full_path) { 'directory' } else { 'file' }
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively search subdirectories
|
||||
if os.is_dir(full_path) {
|
||||
search_directory(full_path, base_path, query_lower, mut results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// APIs
|
||||
@['/api/heroprompt/workspaces'; get]
|
||||
pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result {
|
||||
mut names := []string{}
|
||||
ws := hp.list_workspaces_fromdb() or { []&hp.Workspace{} }
|
||||
for w in ws {
|
||||
names << w.name
|
||||
}
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.text(json.encode(names))
|
||||
}
|
||||
|
||||
// @['/api/heroprompt/workspaces'; post]
|
||||
// pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
|
||||
// name_input := ctx.form['name'] or { '' }
|
||||
// base_path_in := ctx.form['base_path'] or { '' }
|
||||
// if base_path_in.len == 0 {
|
||||
// return ctx.text(json_error('base_path required'))
|
||||
// }
|
||||
|
||||
// base_path := expand_home_path(base_path_in)
|
||||
|
||||
// // If no name provided, generate a random name
|
||||
// mut name := name_input.trim(' \t\n\r')
|
||||
// if name.len == 0 {
|
||||
// name = hp.generate_random_workspace_name()
|
||||
// }
|
||||
|
||||
// wsp := hp.get(name: name, create: true, path: base_path) or {
|
||||
// return ctx.text(json_error('create failed'))
|
||||
// }
|
||||
// ctx.set_content_type('application/json')
|
||||
// return ctx.text(json.encode({
|
||||
// 'name': wsp.name
|
||||
// 'base_path': wsp.base_path
|
||||
// }))
|
||||
// }
|
||||
|
||||
@['/api/heroprompt/workspaces/:name'; get]
|
||||
pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result {
|
||||
wsp := hp.get(name: name, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.text(json.encode({
|
||||
'name': wsp.name
|
||||
'base_path': wsp.base_path
|
||||
'selected_files': wsp.selected_children().len.str()
|
||||
}))
|
||||
}
|
||||
|
||||
@['/api/heroprompt/workspaces/:name'; put]
|
||||
pub fn (app &App) api_heroprompt_update(mut ctx Context, name string) veb.Result {
|
||||
wsp := hp.get(name: name, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
|
||||
new_name := ctx.form['name'] or { name }
|
||||
new_base_path_in := ctx.form['base_path'] or { wsp.base_path }
|
||||
new_base_path := expand_home_path(new_base_path_in)
|
||||
|
||||
// Update the workspace using the update_workspace method
|
||||
updated_wsp := wsp.update_workspace(
|
||||
name: new_name
|
||||
base_path: new_base_path
|
||||
) or { return ctx.text(json_error('failed to update workspace')) }
|
||||
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.text(json.encode({
|
||||
'name': updated_wsp.name
|
||||
'base_path': updated_wsp.base_path
|
||||
}))
|
||||
}
|
||||
|
||||
// Delete endpoint using POST (VEB framework compatibility)
|
||||
@['/api/heroprompt/workspaces/:name/delete'; post]
|
||||
pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result {
|
||||
wsp := hp.get(name: name, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
|
||||
// Delete the workspace
|
||||
wsp.delete_workspace() or { return ctx.text(json_error('failed to delete workspace')) }
|
||||
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.text(json_success())
|
||||
}
|
||||
|
||||
@['/api/heroprompt/directory'; get]
|
||||
pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result {
|
||||
wsname := ctx.query['name'] or { 'default' }
|
||||
path_q := ctx.query['path'] or { '' }
|
||||
mut wsp := hp.get(name: wsname, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
items := wsp.list_dir(path_q) or { return ctx.text(json_error('cannot list directory')) }
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.text(json.encode(DirResp{
|
||||
path: if path_q.len > 0 { path_q } else { wsp.base_path }
|
||||
items: items
|
||||
}))
|
||||
}
|
||||
|
||||
@['/api/heroprompt/file'; get]
|
||||
pub fn (app &App) api_heroprompt_file(mut ctx Context) veb.Result {
|
||||
wsname := ctx.query['name'] or { 'default' }
|
||||
path_q := ctx.query['path'] or { '' }
|
||||
if path_q.len == 0 {
|
||||
return ctx.text(json_error('path required'))
|
||||
}
|
||||
mut base := ''
|
||||
if wsp := hp.get(name: wsname, create: false) {
|
||||
base = wsp.base_path
|
||||
}
|
||||
mut file_path := if !os.is_abs_path(path_q) && base.len > 0 {
|
||||
os.join_path(base, path_q)
|
||||
} else {
|
||||
path_q
|
||||
}
|
||||
if !os.is_file(file_path) {
|
||||
return ctx.text(json_error('not a file'))
|
||||
}
|
||||
content := os.read_file(file_path) or { return ctx.text(json_error('failed to read')) }
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.text(json.encode({
|
||||
'language': detect_lang(file_path)
|
||||
'content': content
|
||||
}))
|
||||
}
|
||||
|
||||
@['/api/heroprompt/workspaces/:name/files'; post]
|
||||
pub fn (app &App) api_heroprompt_add_file(mut ctx Context, name string) veb.Result {
|
||||
path := ctx.form['path'] or { '' }
|
||||
if path.len == 0 {
|
||||
return ctx.text(json_error('path required'))
|
||||
}
|
||||
mut wsp := hp.get(name: name, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
wsp.add_file(path: path) or { return ctx.text(json_error(err.msg())) }
|
||||
return ctx.text(json_success())
|
||||
}
|
||||
|
||||
@['/api/heroprompt/workspaces/:name/dirs'; post]
|
||||
pub fn (app &App) api_heroprompt_add_dir(mut ctx Context, name string) veb.Result {
|
||||
path := ctx.form['path'] or { '' }
|
||||
if path.len == 0 {
|
||||
return ctx.text(json_error('path required'))
|
||||
}
|
||||
mut wsp := hp.get(name: name, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
wsp.add_dir(path: path) or { return ctx.text(json_error(err.msg())) }
|
||||
return ctx.text(json_success())
|
||||
}
|
||||
|
||||
@['/api/heroprompt/workspaces/:name/prompt'; post]
|
||||
pub fn (app &App) api_heroprompt_generate_prompt(mut ctx Context, name string) veb.Result {
|
||||
text := ctx.form['text'] or { '' }
|
||||
mut wsp := hp.get(name: name, create: false) or {
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
prompt := wsp.prompt(text: text)
|
||||
ctx.set_content_type('text/plain')
|
||||
return ctx.text(prompt)
|
||||
}
|
||||
|
||||
@['/api/heroprompt/workspaces/:name/selection'; post]
|
||||
pub fn (app &App) api_heroprompt_sync_selection(mut ctx Context, name string) veb.Result {
|
||||
paths_json := ctx.form['paths'] or { '[]' }
|
||||
mut wsp := hp.get(name: name, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
|
||||
// Clear current selection
|
||||
wsp.children.clear()
|
||||
|
||||
// Parse paths and add them to workspace
|
||||
paths := json.decode([]string, paths_json) or {
|
||||
return ctx.text(json_error('invalid paths format'))
|
||||
}
|
||||
|
||||
for path in paths {
|
||||
if os.is_file(path) {
|
||||
wsp.add_file(path: path) or {
|
||||
continue // Skip files that can't be added
|
||||
}
|
||||
} else if os.is_dir(path) {
|
||||
wsp.add_dir(path: path) or {
|
||||
continue // Skip directories that can't be added
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.text(json_success())
|
||||
}
|
||||
|
||||
@['/api/heroprompt/workspaces/:name/search'; get]
|
||||
pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result {
|
||||
query := ctx.query['q'] or { '' }
|
||||
if query.len == 0 {
|
||||
return ctx.text(json_error('search query required'))
|
||||
}
|
||||
|
||||
wsp := hp.get(name: name, create: false) or {
|
||||
return ctx.text(json_error('workspace not found'))
|
||||
}
|
||||
|
||||
// Simple recursive file search implementation
|
||||
mut results := []map[string]string{}
|
||||
query_lower := query.to_lower()
|
||||
|
||||
// Recursive function to search files
|
||||
search_directory(wsp.base_path, wsp.base_path, query_lower, mut results)
|
||||
|
||||
ctx.set_content_type('application/json')
|
||||
|
||||
// Manually build JSON response to avoid encoding issues
|
||||
mut json_results := '['
|
||||
for i, result in results {
|
||||
if i > 0 {
|
||||
json_results += ','
|
||||
}
|
||||
json_results += '{'
|
||||
json_results += '"name":"${result['name']}",'
|
||||
json_results += '"path":"${result['path']}",'
|
||||
json_results += '"full_path":"${result['full_path']}",'
|
||||
json_results += '"type":"${result['type']}"'
|
||||
json_results += '}'
|
||||
}
|
||||
json_results += ']'
|
||||
|
||||
response := '{"query":"${query}","results":${json_results},"count":"${results.len}"}'
|
||||
return ctx.text(response)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Global state
|
||||
let currentWs = localStorage.getItem('heroprompt-current-ws') || 'default';
|
||||
let selected = new Set();
|
||||
let selected = new Set(); // Selected file paths
|
||||
let selectedDirs = new Set(); // Selected directory paths (for UI state only)
|
||||
let expandedDirs = new Set();
|
||||
let searchQuery = '';
|
||||
|
||||
@@ -141,14 +142,49 @@ class SimpleFileTree {
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'tree-checkbox';
|
||||
checkbox.checked = selected.has(path);
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
checkbox.addEventListener('change', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (checkbox.checked) {
|
||||
selected.add(path);
|
||||
// For directories: mark as selected and select all children files
|
||||
// For files: add the file to selection
|
||||
if (item.type === 'directory') {
|
||||
// Add directory to selectedDirs for UI state tracking
|
||||
selectedDirs.add(path);
|
||||
// Select all file children (adds to 'selected' set)
|
||||
await this.selectDirectoryChildren(path, true);
|
||||
// Auto-expand the directory and its checked subdirectories
|
||||
// This will load subdirectories into the DOM
|
||||
await this.expandDirectoryRecursive(path);
|
||||
// NOW update subdirectory selection after they're in the DOM
|
||||
this.updateSubdirectorySelection(path, true);
|
||||
// Update all checkboxes to reflect the new state
|
||||
this.updateVisibleCheckboxes();
|
||||
} else {
|
||||
selected.add(path);
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
} else {
|
||||
selected.delete(path);
|
||||
// For files: remove from selection
|
||||
// For directories: deselect directory and all children
|
||||
if (item.type === 'directory') {
|
||||
// Remove directory from selectedDirs
|
||||
selectedDirs.delete(path);
|
||||
// Deselect all file children
|
||||
await this.selectDirectoryChildren(path, false);
|
||||
// Update subdirectory selection
|
||||
this.updateSubdirectorySelection(path, false);
|
||||
// Update all checkboxes to reflect the new state
|
||||
this.updateVisibleCheckboxes();
|
||||
// Optionally collapse the directory when unchecked
|
||||
// (commented out to leave expanded for user convenience)
|
||||
// if (this.expandedDirs.has(path)) {
|
||||
// await this.toggleDirectory(path);
|
||||
// }
|
||||
} else {
|
||||
selected.delete(path);
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
}
|
||||
this.updateSelectionUI();
|
||||
});
|
||||
|
||||
// Expand/collapse button for directories
|
||||
@@ -218,11 +254,71 @@ class SimpleFileTree {
|
||||
// Remove from loaded paths so it can be reloaded when expanded again
|
||||
this.loadedPaths.delete(dirPath);
|
||||
} else {
|
||||
// Expand
|
||||
// Expand - update UI optimistically but revert on error
|
||||
this.expandedDirs.add(dirPath);
|
||||
if (expandBtn) expandBtn.innerHTML = '▼';
|
||||
if (icon) icon.textContent = '📂';
|
||||
await this.loadChildren(dirPath);
|
||||
|
||||
// Try to load children
|
||||
const success = await this.loadChildren(dirPath);
|
||||
|
||||
// If loading failed, revert the UI state
|
||||
if (!success) {
|
||||
this.expandedDirs.delete(dirPath);
|
||||
if (expandBtn) expandBtn.innerHTML = '▶';
|
||||
if (icon) icon.textContent = '📁';
|
||||
} else {
|
||||
// Loading succeeded - restore checkbox states for subdirectories
|
||||
// Check if this directory or any parent is selected
|
||||
const isSelected = selectedDirs.has(dirPath) || this.isParentDirectorySelected(dirPath);
|
||||
|
||||
if (isSelected) {
|
||||
// Restore subdirectory selection states
|
||||
this.updateSubdirectorySelection(dirPath, true);
|
||||
}
|
||||
|
||||
// Update all visible checkboxes to reflect current state
|
||||
this.updateVisibleCheckboxes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand a directory if it's not already expanded
|
||||
async expandDirectory(dirPath) {
|
||||
const isExpanded = this.expandedDirs.has(dirPath);
|
||||
if (!isExpanded) {
|
||||
await this.toggleDirectory(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively expand a directory and all its checked subdirectories
|
||||
async expandDirectoryRecursive(dirPath) {
|
||||
// First, expand this directory
|
||||
await this.expandDirectory(dirPath);
|
||||
|
||||
// Wait a bit for the DOM to update with children
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Find all subdirectories that are children of this directory
|
||||
const childDirs = Array.from(document.querySelectorAll('.tree-item'))
|
||||
.filter(item => {
|
||||
const itemPath = item.dataset.path;
|
||||
const itemType = item.dataset.type;
|
||||
// Check if this is a direct or indirect child directory
|
||||
return itemType === 'directory' &&
|
||||
itemPath !== dirPath &&
|
||||
itemPath.startsWith(dirPath + '/');
|
||||
});
|
||||
|
||||
// Recursively expand checked subdirectories
|
||||
for (const childDir of childDirs) {
|
||||
const childPath = childDir.dataset.path;
|
||||
const checkbox = childDir.querySelector('.tree-checkbox');
|
||||
|
||||
// If the subdirectory checkbox is checked, expand it recursively
|
||||
if (checkbox && checkbox.checked) {
|
||||
await this.expandDirectoryRecursive(childPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,11 +348,11 @@ class SimpleFileTree {
|
||||
async loadChildren(parentPath) {
|
||||
// Always reload children to ensure fresh data
|
||||
console.log('Loading children for:', parentPath);
|
||||
const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`);
|
||||
const r = await api(`/api/heroprompt/directory?name=${currentWs}&base=${encodeURIComponent(parentPath)}&path=`);
|
||||
|
||||
if (r.error) {
|
||||
console.warn('Failed to load directory:', parentPath, r.error);
|
||||
return;
|
||||
return false; // Return false to indicate failure
|
||||
}
|
||||
|
||||
// Sort items: directories first, then files
|
||||
@@ -271,7 +367,7 @@ class SimpleFileTree {
|
||||
const parentElement = qs(`[data-path="${parentPath}"]`);
|
||||
if (!parentElement) {
|
||||
console.warn('Parent element not found for path:', parentPath);
|
||||
return;
|
||||
return false; // Return false to indicate failure
|
||||
}
|
||||
|
||||
const parentDepth = parseInt(parentElement.dataset.depth || '0');
|
||||
@@ -316,6 +412,7 @@ class SimpleFileTree {
|
||||
});
|
||||
|
||||
this.loadedPaths.add(parentPath);
|
||||
return true; // Return true to indicate success
|
||||
}
|
||||
|
||||
getDepth(path) {
|
||||
@@ -378,6 +475,127 @@ class SimpleFileTree {
|
||||
if (tokenCountEl) tokenCountEl.textContent = tokens.toString();
|
||||
}
|
||||
|
||||
// Select or deselect all children of a directory recursively
|
||||
async selectDirectoryChildren(dirPath, select) {
|
||||
// Get all children from API to update the selection state
|
||||
// This only selects FILES, not directories (API returns only files)
|
||||
await this.selectDirectoryChildrenFromAPI(dirPath, select);
|
||||
|
||||
// Note: We don't call updateSubdirectorySelection() here because
|
||||
// subdirectories might not be in the DOM yet. The caller should
|
||||
// call it after expanding the directory.
|
||||
|
||||
// Update any currently visible children in the DOM
|
||||
this.updateVisibleCheckboxes();
|
||||
|
||||
// Update the selection UI once at the end
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
|
||||
// Update selectedDirs for all subdirectories under a path
|
||||
updateSubdirectorySelection(dirPath, select) {
|
||||
// Find all visible subdirectories under this path
|
||||
const treeItems = document.querySelectorAll('.tree-item');
|
||||
treeItems.forEach(item => {
|
||||
const itemPath = item.dataset.path;
|
||||
const itemType = item.dataset.type;
|
||||
|
||||
// Check if this is a subdirectory of dirPath
|
||||
if (itemType === 'directory' && itemPath !== dirPath && itemPath.startsWith(dirPath + '/')) {
|
||||
if (select) {
|
||||
selectedDirs.add(itemPath);
|
||||
} else {
|
||||
selectedDirs.delete(itemPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update all visible checkboxes to match the current selection state
|
||||
updateVisibleCheckboxes() {
|
||||
const treeItems = document.querySelectorAll('.tree-item');
|
||||
|
||||
treeItems.forEach(item => {
|
||||
const itemPath = item.dataset.path;
|
||||
const itemType = item.dataset.type;
|
||||
const checkbox = item.querySelector('.tree-checkbox');
|
||||
|
||||
if (checkbox && itemPath) {
|
||||
if (itemType === 'file') {
|
||||
// For files: check if the file path is in selected set
|
||||
checkbox.checked = selected.has(itemPath);
|
||||
} else if (itemType === 'directory') {
|
||||
// For directories: check if all file children are selected
|
||||
checkbox.checked = this.areAllChildrenSelected(itemPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if a directory should be checked
|
||||
// A directory is checked if:
|
||||
// 1. It's in the selectedDirs set (explicitly selected), OR
|
||||
// 2. Any parent directory is in selectedDirs (cascading)
|
||||
areAllChildrenSelected(dirPath) {
|
||||
// Check if this directory is explicitly selected
|
||||
if (selectedDirs.has(dirPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any parent directory is selected (cascading)
|
||||
if (this.isParentDirectorySelected(dirPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if any parent directory of this path is selected
|
||||
isParentDirectorySelected(dirPath) {
|
||||
// Walk up the directory tree
|
||||
let currentPath = dirPath;
|
||||
|
||||
while (currentPath.includes('/')) {
|
||||
// Get parent directory
|
||||
const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/'));
|
||||
|
||||
// Check if parent is in selectedDirs
|
||||
if (selectedDirs.has(parentPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentPath = parentPath;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select directory children using API to get complete recursive list
|
||||
async selectDirectoryChildrenFromAPI(dirPath, select) {
|
||||
try {
|
||||
const response = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/list?path=${encodeURIComponent(dirPath)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.children) {
|
||||
data.children.forEach(child => {
|
||||
const childPath = child.path;
|
||||
if (select) {
|
||||
selected.add(childPath);
|
||||
} else {
|
||||
selected.delete(childPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch directory children:', response.status, response.statusText);
|
||||
const errorText = await response.text();
|
||||
console.error('Error response:', errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting directory children:', error);
|
||||
}
|
||||
}
|
||||
|
||||
createFileCard(path) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'file-card';
|
||||
@@ -395,6 +613,9 @@ class SimpleFileTree {
|
||||
// Get file stats (mock data for now - could be enhanced with real file stats)
|
||||
const stats = this.getFileStats(path);
|
||||
|
||||
// Show full path for directories to help differentiate between same-named directories
|
||||
const displayPath = isDirectory ? path : path;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="file-card-header">
|
||||
<div class="file-card-icon">
|
||||
@@ -402,7 +623,7 @@ class SimpleFileTree {
|
||||
</div>
|
||||
<div class="file-card-info">
|
||||
<h4 class="file-card-name">${fileName}</h4>
|
||||
<p class="file-card-path">${path}</p>
|
||||
<p class="file-card-path">${displayPath}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-card-metadata">
|
||||
@@ -655,14 +876,22 @@ class SimpleFileTree {
|
||||
selectAll() {
|
||||
qsa('.tree-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = true;
|
||||
const path = checkbox.closest('.tree-item').dataset.path;
|
||||
selected.add(path);
|
||||
const treeItem = checkbox.closest('.tree-item');
|
||||
const path = treeItem.dataset.path;
|
||||
const type = treeItem.dataset.type;
|
||||
// Add files to selected set, directories to selectedDirs set
|
||||
if (type === 'file') {
|
||||
selected.add(path);
|
||||
} else if (type === 'directory') {
|
||||
selectedDirs.add(path);
|
||||
}
|
||||
});
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
selected.clear();
|
||||
selectedDirs.clear();
|
||||
qsa('.tree-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
@@ -684,6 +913,26 @@ class SimpleFileTree {
|
||||
this.loadedPaths.clear();
|
||||
}
|
||||
|
||||
// Refresh a specific directory (collapse and re-expand to reload its contents)
|
||||
async refreshDirectory(dirPath) {
|
||||
const dirElement = document.querySelector(`[data-path="${dirPath}"][data-type="directory"]`);
|
||||
if (!dirElement) {
|
||||
console.warn('Directory element not found:', dirPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const wasExpanded = this.expandedDirs.has(dirPath);
|
||||
|
||||
if (wasExpanded) {
|
||||
// Collapse the directory
|
||||
await this.toggleDirectory(dirPath);
|
||||
// Wait a bit
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
// Re-expand to reload contents
|
||||
await this.toggleDirectory(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
async search(query) {
|
||||
searchQuery = query.toLowerCase().trim();
|
||||
|
||||
@@ -825,6 +1074,66 @@ class SimpleFileTree {
|
||||
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
|
||||
async renderWorkspaceDirectories(directories) {
|
||||
this.container.innerHTML = '<div class="loading">Loading workspace directories...</div>';
|
||||
|
||||
// Reset state
|
||||
this.loadedPaths.clear();
|
||||
this.expandedDirs.clear();
|
||||
expandedDirs.clear();
|
||||
|
||||
if (!directories || directories.length === 0) {
|
||||
this.container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="icon-folder-open"></i>
|
||||
<p>No directories added yet</p>
|
||||
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create document fragment for efficient DOM manipulation
|
||||
const fragment = document.createDocumentFragment();
|
||||
const elements = [];
|
||||
|
||||
// Create elements for each workspace directory
|
||||
for (const dir of directories) {
|
||||
if (!dir.path || dir.path.cat !== 'dir') continue;
|
||||
|
||||
const dirPath = dir.path.path;
|
||||
const dirName = dir.name || dirPath.split('/').pop();
|
||||
|
||||
// Create a directory item that can be expanded
|
||||
const item = {
|
||||
name: dirName,
|
||||
type: 'directory'
|
||||
};
|
||||
|
||||
const element = this.createFileItem(item, dirPath, 0);
|
||||
element.style.opacity = '0';
|
||||
element.style.transform = 'translateY(-10px)';
|
||||
element.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
|
||||
fragment.appendChild(element);
|
||||
elements.push(element);
|
||||
}
|
||||
|
||||
// Clear container and add all elements at once
|
||||
this.container.innerHTML = '';
|
||||
this.container.appendChild(fragment);
|
||||
|
||||
// Trigger staggered animations
|
||||
elements.forEach((element, i) => {
|
||||
setTimeout(() => {
|
||||
element.style.opacity = '1';
|
||||
element.style.transform = 'translateY(0)';
|
||||
}, i * 50);
|
||||
});
|
||||
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
}
|
||||
|
||||
// Global tree instance
|
||||
@@ -886,10 +1195,56 @@ async function initWorkspace() {
|
||||
const sel = el('workspaceSelect');
|
||||
if (sel) sel.value = currentWs;
|
||||
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
// Load and display workspace directories
|
||||
await loadWorkspaceDirectories();
|
||||
}
|
||||
|
||||
async function loadWorkspaceDirectories() {
|
||||
const treeEl = el('tree');
|
||||
if (!treeEl) return;
|
||||
|
||||
try {
|
||||
const children = await api(`/api/heroprompt/workspaces/${currentWs}/children`);
|
||||
|
||||
if (children.error) {
|
||||
console.warn('Failed to load workspace children:', children.error);
|
||||
treeEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="icon-folder-open"></i>
|
||||
<p>No directories added yet</p>
|
||||
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter only directories
|
||||
const directories = children.filter(child => child.path && child.path.cat === 'dir');
|
||||
|
||||
if (directories.length === 0) {
|
||||
treeEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="icon-folder-open"></i>
|
||||
<p>No directories added yet</p>
|
||||
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create file tree with workspace directories as roots
|
||||
if (fileTree) {
|
||||
await fileTree.renderWorkspaceDirectories(directories);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading workspace directories:', error);
|
||||
treeEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="icon-folder-open"></i>
|
||||
<p>Error loading directories</p>
|
||||
<small>Please try refreshing the page</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -917,19 +1272,15 @@ async function generatePrompt() {
|
||||
outputEl.innerHTML = '<div class="loading">Generating prompt...</div>';
|
||||
|
||||
try {
|
||||
// sync selection to backend before generating
|
||||
// Pass selections directly to prompt generation
|
||||
const paths = Array.from(selected);
|
||||
const syncResult = await post(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/selection`, {
|
||||
paths: JSON.stringify(paths)
|
||||
});
|
||||
|
||||
if (syncResult.error) {
|
||||
throw new Error(`Failed to sync selection: ${syncResult.error}`);
|
||||
}
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('text', promptText);
|
||||
formData.append('selected_paths', JSON.stringify(paths));
|
||||
|
||||
const r = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/prompt`, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ text: promptText })
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -952,41 +1303,64 @@ async function copyPrompt() {
|
||||
const outputEl = el('promptOutput');
|
||||
if (!outputEl) {
|
||||
console.warn('Prompt output element not found');
|
||||
showStatus('Copy failed - element not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const text = outputEl.textContent;
|
||||
console.log('text', text);
|
||||
if (!text || text.trim().length === 0 || text.includes('No files selected') || text.includes('Generated prompt will appear here')) {
|
||||
console.warn('No valid content to copy');
|
||||
// Grab the visible prompt text, stripping HTML and empty-state placeholders
|
||||
const text = outputEl.innerText.trim();
|
||||
if (!text || text.includes('Generated prompt will appear here') || text.includes('No files selected')) {
|
||||
showStatus('Nothing to copy', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navigator.clipboard) {
|
||||
// Fallback for older browsers
|
||||
fallbackCopyToClipboard(text);
|
||||
return;
|
||||
// Try the modern Clipboard API first
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showStatus('Prompt copied to clipboard!', 'success');
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('Clipboard API failed, falling back', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to hidden textarea method
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed'; // avoid scrolling to bottom
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
// Show success feedback
|
||||
const originalContent = outputEl.innerHTML;
|
||||
outputEl.innerHTML = '<div class="success-message">Prompt copied to clipboard!</div>';
|
||||
setTimeout(() => {
|
||||
outputEl.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
const successful = document.execCommand('copy');
|
||||
showStatus(successful ? 'Prompt copied!' : 'Copy failed', successful ? 'success' : 'error');
|
||||
} catch (e) {
|
||||
console.warn('Copy failed', e);
|
||||
const originalContent = outputEl.innerHTML;
|
||||
outputEl.innerHTML = '<div class="error-message">Failed to copy prompt</div>';
|
||||
setTimeout(() => {
|
||||
outputEl.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
console.error('Fallback copy failed', e);
|
||||
showStatus('Copy failed', 'error');
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper – show a transient message inside the output pane */
|
||||
function showStatus(msg, type = 'info') {
|
||||
const out = el('promptOutput');
|
||||
if (!out) return;
|
||||
|
||||
const original = out.innerHTML;
|
||||
const statusClass = type === 'success' ? 'success-message' :
|
||||
type === 'error' ? 'error-message' :
|
||||
type === 'warning' ? 'warning-message' : 'info-message';
|
||||
|
||||
out.innerHTML = `<div class="${statusClass}">${msg}</div>`;
|
||||
setTimeout(() => {
|
||||
out.innerHTML = original;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Global fallback function for clipboard operations
|
||||
function fallbackCopyToClipboard(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
@@ -1049,11 +1423,9 @@ async function deleteWorkspace(workspaceName) {
|
||||
currentWs = names[0];
|
||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||
await reloadWorkspaces();
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
}
|
||||
|
||||
// Load directories for new current workspace
|
||||
await loadWorkspaceDirectories();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,15 +1436,12 @@ async function deleteWorkspace(workspaceName) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWorkspace(workspaceName, newName, newPath) {
|
||||
async function updateWorkspace(workspaceName, newName) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
if (newName && newName !== workspaceName) {
|
||||
formData.append('name', newName);
|
||||
}
|
||||
if (newPath) {
|
||||
formData.append('base_path', newPath);
|
||||
}
|
||||
|
||||
const encodedName = encodeURIComponent(workspaceName);
|
||||
const response = await fetch(`/api/heroprompt/workspaces/${encodedName}`, {
|
||||
@@ -1095,12 +1464,6 @@ async function updateWorkspace(workspaceName, newName, newPath) {
|
||||
}
|
||||
|
||||
await reloadWorkspaces();
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.warn('Update workspace failed', e);
|
||||
@@ -1135,11 +1498,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
workspaceSelect.addEventListener('change', async (e) => {
|
||||
currentWs = e.target.value;
|
||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
}
|
||||
|
||||
// Load directories for the new workspace
|
||||
await loadWorkspaceDirectories();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1154,10 +1515,25 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const refreshExplorerBtn = el('refreshExplorer');
|
||||
if (refreshExplorerBtn) {
|
||||
refreshExplorerBtn.addEventListener('click', async () => {
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
// Save currently expanded directories before refresh
|
||||
const previouslyExpanded = new Set(expandedDirs);
|
||||
|
||||
// Reload workspace directories
|
||||
await loadWorkspaceDirectories();
|
||||
|
||||
// Re-expand previously expanded directories
|
||||
if (fileTree && previouslyExpanded.size > 0) {
|
||||
// Wait a bit for the DOM to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Re-expand each previously expanded directory
|
||||
for (const dirPath of previouslyExpanded) {
|
||||
const dirElement = document.querySelector(`[data-path="${dirPath}"][data-type="directory"]`);
|
||||
if (dirElement && !expandedDirs.has(dirPath)) {
|
||||
// Expand this directory
|
||||
await fileTree.toggleDirectory(dirPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1222,11 +1598,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (wsCreateBtn) {
|
||||
wsCreateBtn.addEventListener('click', () => {
|
||||
const nameEl = el('wcName');
|
||||
const pathEl = el('wcPath');
|
||||
const errorEl = el('wcError');
|
||||
|
||||
if (nameEl) nameEl.value = '';
|
||||
if (pathEl) pathEl.value = '';
|
||||
if (errorEl) errorEl.textContent = '';
|
||||
|
||||
showModal('wsCreate');
|
||||
@@ -1237,16 +1611,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (wcCreateBtn) {
|
||||
wcCreateBtn.addEventListener('click', async () => {
|
||||
const name = el('wcName')?.value?.trim() || '';
|
||||
const path = el('wcPath')?.value?.trim() || '';
|
||||
const errorEl = el('wcError');
|
||||
|
||||
if (!path) {
|
||||
if (errorEl) errorEl.textContent = 'Path is required.';
|
||||
if (!name) {
|
||||
if (errorEl) errorEl.textContent = 'Workspace name is required.';
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = { base_path: path };
|
||||
if (name) formData.name = name;
|
||||
const formData = { name: name };
|
||||
|
||||
const resp = await post('/api/heroprompt/workspaces', formData);
|
||||
if (resp.error) {
|
||||
@@ -1258,10 +1630,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
localStorage.setItem('heroprompt-current-ws', currentWs);
|
||||
await reloadWorkspaces();
|
||||
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
const base = info?.base_path || '';
|
||||
if (base && fileTree) {
|
||||
await fileTree.render(base);
|
||||
// Clear the file tree since new workspace has no directories yet
|
||||
if (fileTree) {
|
||||
const treeEl = el('tree');
|
||||
if (treeEl) {
|
||||
treeEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="icon-folder-open"></i>
|
||||
<p>No directories added yet</p>
|
||||
<small>Use the "Add Dir" button to add directories to this workspace</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
hideModal('wsCreate');
|
||||
@@ -1275,11 +1655,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const info = await api(`/api/heroprompt/workspaces/${currentWs}`);
|
||||
if (info && !info.error) {
|
||||
const nameEl = el('wdName');
|
||||
const pathEl = el('wdPath');
|
||||
const errorEl = el('wdError');
|
||||
|
||||
if (nameEl) nameEl.value = info.name || currentWs;
|
||||
if (pathEl) pathEl.value = info.base_path || '';
|
||||
if (errorEl) errorEl.textContent = '';
|
||||
|
||||
showModal('wsDetails');
|
||||
@@ -1292,15 +1670,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (wdUpdateBtn) {
|
||||
wdUpdateBtn.addEventListener('click', async () => {
|
||||
const name = el('wdName')?.value?.trim() || '';
|
||||
const path = el('wdPath')?.value?.trim() || '';
|
||||
const errorEl = el('wdError');
|
||||
|
||||
if (!path) {
|
||||
if (errorEl) errorEl.textContent = 'Path is required.';
|
||||
if (!name) {
|
||||
if (errorEl) errorEl.textContent = 'Workspace name is required.';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await updateWorkspace(currentWs, name, path);
|
||||
const result = await updateWorkspace(currentWs, name);
|
||||
if (result.error) {
|
||||
if (errorEl) errorEl.textContent = result.error;
|
||||
return;
|
||||
@@ -1326,6 +1703,52 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
// Add Directory functionality
|
||||
const addDirBtn = el('addDirBtn');
|
||||
if (addDirBtn) {
|
||||
addDirBtn.addEventListener('click', () => {
|
||||
const pathEl = el('addDirPath');
|
||||
const errorEl = el('addDirError');
|
||||
|
||||
if (pathEl) pathEl.value = '';
|
||||
if (errorEl) errorEl.textContent = '';
|
||||
|
||||
showModal('addDirModal');
|
||||
});
|
||||
}
|
||||
|
||||
const addDirConfirm = el('addDirConfirm');
|
||||
if (addDirConfirm) {
|
||||
addDirConfirm.addEventListener('click', async () => {
|
||||
const path = el('addDirPath')?.value?.trim() || '';
|
||||
const errorEl = el('addDirError');
|
||||
|
||||
if (!path) {
|
||||
if (errorEl) errorEl.textContent = 'Directory path is required.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Add directory via API
|
||||
const result = await post(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/dirs`, {
|
||||
path: path
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
if (errorEl) errorEl.textContent = result.error;
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - close modal and refresh the file tree
|
||||
hideModal('addDirModal');
|
||||
|
||||
// Reload workspace directories to show the newly added directory
|
||||
await loadWorkspaceDirectories();
|
||||
|
||||
// Show success message
|
||||
showStatus('Directory added successfully!', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
// Chat functionality
|
||||
initChatInterface();
|
||||
});
|
||||
|
||||
@@ -86,6 +86,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="selection-actions">
|
||||
<button id="addDirBtn" class="btn btn-sm btn-primary" title="Add Directory">
|
||||
<i class="icon-plus"></i>
|
||||
Add Dir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -310,12 +314,10 @@ Example:
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="wcName" class="form-label">Workspace Name (optional)</label>
|
||||
<input type="text" class="form-control" id="wcName" placeholder="Enter workspace name">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="wcPath" class="form-label">Base Path (required)</label>
|
||||
<input type="text" class="form-control" id="wcPath" placeholder="Enter base directory path">
|
||||
<label for="wcName" class="form-label">Workspace Name (required)</label>
|
||||
<input type="text" class="form-control" id="wcName" placeholder="Enter workspace name" required>
|
||||
<div class="form-text">Choose a unique name for your workspace. You can add directories to it
|
||||
after creation.</div>
|
||||
</div>
|
||||
<div id="wcError" class="text-danger small"></div>
|
||||
</div>
|
||||
@@ -340,10 +342,6 @@ Example:
|
||||
<label for="wdName" class="form-label">Workspace Name</label>
|
||||
<input type="text" class="form-control" id="wdName">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="wdPath" class="form-label">Base Path</label>
|
||||
<input type="text" class="form-control" id="wdPath">
|
||||
</div>
|
||||
<div id="wdError" class="text-danger small"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -392,6 +390,32 @@ Example:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Directory Modal -->
|
||||
<div class="modal fade" id="addDirModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Directory to Workspace</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="addDirPath" class="form-label">Directory Path</label>
|
||||
<input type="text" class="form-control" id="addDirPath"
|
||||
placeholder="Enter directory path (e.g., /path/to/directory)">
|
||||
<div class="form-text">Enter the full path to the directory you want to add to the workspace.
|
||||
</div>
|
||||
</div>
|
||||
<div id="addDirError" class="text-danger small"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="addDirConfirm">Add Directory</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
Reference in New Issue
Block a user