feat: Add recursive directory selection and enhance prompt builder
- Add `select_all` option to recursively add directory contents - Implement `select_all_files_and_dirs` for file traversal - Rework prompt building with file tree and content formatters - Improve `get_file_extension` to handle dotfiles and special files - Update prompt template to use new structured data model
This commit is contained in:
@@ -3,12 +3,14 @@ module heroprompt
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
// Parameters for adding a file to a directory
|
||||
@[params]
|
||||
pub struct AddFileParams {
|
||||
pub mut:
|
||||
name string
|
||||
name string // Name of the file to select
|
||||
}
|
||||
|
||||
// select_file adds a specific file to the directory's selected files list
|
||||
pub fn (mut dir HeropromptDir) select_file(args AddFileParams) !&HeropromptFile {
|
||||
mut full_path := dir.path.path + '/' + args.name
|
||||
if dir.path.path.ends_with('/') {
|
||||
@@ -38,3 +40,46 @@ pub fn (mut dir HeropromptDir) select_file(args AddFileParams) !&HeropromptFile
|
||||
dir.files << file
|
||||
return file
|
||||
}
|
||||
|
||||
// select_all_files_and_dirs recursively selects all files and subdirectories
|
||||
// from the given path and adds them to the current directory structure
|
||||
pub fn (mut dir HeropromptDir) select_all_files_and_dirs(path string) {
|
||||
// First, get all immediate children (files and directories) of the current path
|
||||
entries := os.ls(path) or { return }
|
||||
|
||||
for entry in entries {
|
||||
full_path := os.join_path(path, entry)
|
||||
|
||||
if os.is_dir(full_path) {
|
||||
// Create subdirectory
|
||||
mut sub_dir := &HeropromptDir{
|
||||
path: pathlib.Path{
|
||||
path: full_path
|
||||
cat: .dir
|
||||
exist: .yes
|
||||
}
|
||||
name: entry
|
||||
}
|
||||
|
||||
// Recursively populate the subdirectory
|
||||
sub_dir.select_all_files_and_dirs(full_path)
|
||||
|
||||
// Add subdirectory to current directory
|
||||
dir.dirs << sub_dir
|
||||
} else if os.is_file(full_path) {
|
||||
// Read file content when selecting all
|
||||
file_content := os.read_file(full_path) or { '' }
|
||||
|
||||
file := &HeropromptFile{
|
||||
path: pathlib.Path{
|
||||
path: full_path
|
||||
cat: .file
|
||||
exist: .yes
|
||||
}
|
||||
name: entry
|
||||
content: file_content
|
||||
}
|
||||
dir.files << file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,59 @@
|
||||
module heroprompt
|
||||
|
||||
// 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'
|
||||
}
|
||||
|
||||
// Convert to lowercase for comparison
|
||||
lower_filename := filename.to_lower()
|
||||
|
||||
// Check if it's a special file without extension
|
||||
if lower_filename in special_files {
|
||||
return special_files[lower_filename]
|
||||
}
|
||||
|
||||
// Handle dotfiles (files starting with .)
|
||||
if filename.starts_with('.') && !filename.starts_with('..') {
|
||||
// For files like .gitignore, .bashrc, etc.
|
||||
if filename.contains('.') && filename.len > 1 {
|
||||
parts := filename[1..].split('.')
|
||||
if parts.len >= 2 {
|
||||
return parts[parts.len - 1]
|
||||
} else {
|
||||
// Files like .gitignore, .bashrc (treat the whole name as extension type)
|
||||
return filename[1..]
|
||||
}
|
||||
} else {
|
||||
// Single dot files
|
||||
return filename[1..]
|
||||
}
|
||||
}
|
||||
|
||||
// Regular files with extensions
|
||||
parts := filename.split('.')
|
||||
if parts.len < 2 {
|
||||
// Files with no extension - return empty string
|
||||
return ''
|
||||
}
|
||||
|
||||
return parts[parts.len - 1]
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ module heroprompt
|
||||
|
||||
import rand
|
||||
|
||||
// HeropromptSession manages multiple workspaces for organizing AI prompts
|
||||
pub struct HeropromptSession {
|
||||
pub mut:
|
||||
id string
|
||||
workspaces []&HeropromptWorkspace
|
||||
id string // Unique session identifier
|
||||
workspaces []&HeropromptWorkspace // List of workspaces in this session
|
||||
}
|
||||
|
||||
// new_session creates a new heroprompt session with a unique ID
|
||||
pub fn new_session() HeropromptSession {
|
||||
return HeropromptSession{
|
||||
id: rand.uuid_v4()
|
||||
@@ -15,6 +17,7 @@ pub fn new_session() HeropromptSession {
|
||||
}
|
||||
}
|
||||
|
||||
// add_workspace creates and adds a new workspace to the session
|
||||
pub fn (mut self HeropromptSession) add_workspace(args_ NewWorkspaceParams) !&HeropromptWorkspace {
|
||||
mut wsp := &HeropromptWorkspace{}
|
||||
wsp = wsp.new(args_)!
|
||||
|
||||
@@ -5,11 +5,13 @@ import time
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
// HeropromptWorkspace represents a workspace containing multiple directories
|
||||
// and their selected files for AI prompt generation
|
||||
@[heap]
|
||||
pub struct HeropromptWorkspace {
|
||||
pub mut:
|
||||
name string = 'default'
|
||||
dirs []&HeropromptDir
|
||||
name string = 'default' // Workspace name
|
||||
dirs []&HeropromptDir // List of directories in this workspace
|
||||
}
|
||||
|
||||
@[params]
|
||||
@@ -33,7 +35,8 @@ fn (wsp HeropromptWorkspace) new(args_ NewWorkspaceParams) !&HeropromptWorkspace
|
||||
@[params]
|
||||
pub struct AddDirParams {
|
||||
pub mut:
|
||||
path string @[required]
|
||||
path string @[required]
|
||||
select_all bool
|
||||
}
|
||||
|
||||
pub fn (mut wsp HeropromptWorkspace) add_dir(args_ AddDirParams) !&HeropromptDir {
|
||||
@@ -51,7 +54,7 @@ pub fn (mut wsp HeropromptWorkspace) add_dir(args_ AddDirParams) !&HeropromptDir
|
||||
parts := abs_path.split(os.path_separator)
|
||||
dir_name := parts[parts.len - 1]
|
||||
|
||||
added_dir := &HeropromptDir{
|
||||
mut added_dir := &HeropromptDir{
|
||||
path: pathlib.Path{
|
||||
path: abs_path
|
||||
cat: .dir
|
||||
@@ -60,25 +63,30 @@ pub fn (mut wsp HeropromptWorkspace) add_dir(args_ AddDirParams) !&HeropromptDir
|
||||
name: dir_name
|
||||
}
|
||||
|
||||
if args_.select_all {
|
||||
added_dir.select_all_files_and_dirs(abs_path)
|
||||
}
|
||||
|
||||
wsp.dirs << added_dir
|
||||
return added_dir
|
||||
}
|
||||
|
||||
// Metadata structures for selected files and directories
|
||||
struct SelectedFilesMetadata {
|
||||
content_length int
|
||||
extension string
|
||||
name string
|
||||
path string
|
||||
content_length int // File content length in characters
|
||||
extension string // File extension
|
||||
name string // File name
|
||||
path string // Full file path
|
||||
}
|
||||
|
||||
struct SelectedDirsMetadata {
|
||||
name string
|
||||
selected_files []SelectedFilesMetadata
|
||||
name string // Directory name
|
||||
selected_files []SelectedFilesMetadata // Files in this directory
|
||||
}
|
||||
|
||||
struct HeropromptWorkspaceGetSelected {
|
||||
pub mut:
|
||||
dirs []SelectedDirsMetadata
|
||||
dirs []SelectedDirsMetadata // All directories with their selected files
|
||||
}
|
||||
|
||||
pub fn (wsp HeropromptWorkspace) get_selected() HeropromptWorkspaceGetSelected {
|
||||
@@ -119,7 +127,8 @@ fn (wsp HeropromptWorkspace) build_user_instructions(text string) string {
|
||||
return text
|
||||
}
|
||||
|
||||
fn build_file_map(dirs []&HeropromptDir, prefix string) string {
|
||||
// build_file_tree creates a tree-like representation of directories and files
|
||||
fn build_file_tree(dirs []&HeropromptDir, prefix string) string {
|
||||
mut out := ''
|
||||
|
||||
for i, dir in dirs {
|
||||
@@ -129,36 +138,194 @@ fn build_file_map(dirs []&HeropromptDir, prefix string) string {
|
||||
// Directory name
|
||||
out += '${prefix}${connector}${dir.name}\n'
|
||||
|
||||
// Calculate new prefix for children
|
||||
child_prefix := if i == dirs.len - 1 { prefix + ' ' } else { prefix + '│ ' }
|
||||
|
||||
// Count total children (files + subdirs) for proper tree formatting
|
||||
total_children := dir.files.len + dir.dirs.len
|
||||
|
||||
// Files in this directory
|
||||
for j, file in dir.files {
|
||||
file_connector := if j == dir.files.len - 1 && dir.dirs.len == 0 {
|
||||
file_connector := if j == total_children - 1 { '└── ' } else { '├── ' }
|
||||
out += '${child_prefix}${file_connector}${file.name} *\n'
|
||||
}
|
||||
|
||||
// Recurse into subdirectories
|
||||
for j, sub_dir in dir.dirs {
|
||||
sub_connector := if dir.files.len + j == total_children - 1 {
|
||||
'└── '
|
||||
} else {
|
||||
'├── '
|
||||
}
|
||||
out += '${prefix} ${file_connector}${file.name} *\n'
|
||||
}
|
||||
out += '${child_prefix}${sub_connector}${sub_dir.name}\n'
|
||||
|
||||
// Recurse into subdirectories
|
||||
if dir.dirs.len > 0 {
|
||||
new_prefix := if i == dirs.len - 1 { prefix + ' ' } else { prefix + '│ ' }
|
||||
out += build_file_map(dir.dirs, new_prefix)
|
||||
// Recursive call for subdirectory contents
|
||||
sub_prefix := if dir.files.len + j == total_children - 1 {
|
||||
child_prefix + ' '
|
||||
} else {
|
||||
child_prefix + '│ '
|
||||
}
|
||||
|
||||
// Build content for this subdirectory directly without calling build_file_map again
|
||||
sub_total_children := sub_dir.files.len + sub_dir.dirs.len
|
||||
|
||||
// Files in subdirectory
|
||||
for k, sub_file in sub_dir.files {
|
||||
sub_file_connector := if k == sub_total_children - 1 {
|
||||
'└── '
|
||||
} else {
|
||||
'├── '
|
||||
}
|
||||
out += '${sub_prefix}${sub_file_connector}${sub_file.name} *\n'
|
||||
}
|
||||
|
||||
// Recursively handle deeper subdirectories
|
||||
if sub_dir.dirs.len > 0 {
|
||||
out += build_file_tree(sub_dir.dirs, sub_prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// build_file_content generates formatted content for all selected files
|
||||
fn (wsp HeropromptWorkspace) build_file_content() string {
|
||||
return ''
|
||||
mut content := ''
|
||||
|
||||
for dir in wsp.dirs {
|
||||
// Process files in current directory
|
||||
for file in dir.files {
|
||||
if content.len > 0 {
|
||||
content += '\n\n'
|
||||
}
|
||||
|
||||
// File path
|
||||
content += '${file.path.path}\n'
|
||||
|
||||
// File content with syntax highlighting or empty file info
|
||||
extension := get_file_extension(file.name)
|
||||
if file.content.len == 0 {
|
||||
content += '(Empty file)\n'
|
||||
} else {
|
||||
content += '```${extension}\n'
|
||||
content += file.content
|
||||
content += '\n```'
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process subdirectories
|
||||
content += wsp.build_dir_file_content(dir.dirs)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// build_dir_file_content recursively processes subdirectories
|
||||
fn (wsp HeropromptWorkspace) build_dir_file_content(dirs []&HeropromptDir) string {
|
||||
mut content := ''
|
||||
|
||||
for dir in dirs {
|
||||
// Process files in current directory
|
||||
for file in dir.files {
|
||||
if content.len > 0 {
|
||||
content += '\n\n'
|
||||
}
|
||||
|
||||
// File path
|
||||
content += '${file.path.path}\n'
|
||||
|
||||
// File content with syntax highlighting or empty file info
|
||||
extension := get_file_extension(file.name)
|
||||
if file.content.len == 0 {
|
||||
content += '(Empty file)\n'
|
||||
} else {
|
||||
content += '```${extension}\n'
|
||||
content += file.content
|
||||
content += '\n```'
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process subdirectories
|
||||
if dir.dirs.len > 0 {
|
||||
content += wsp.build_dir_file_content(dir.dirs)
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
pub struct HeropromptTmpPrompt {
|
||||
pub mut:
|
||||
user_instructions string
|
||||
file_map string
|
||||
file_contents string
|
||||
}
|
||||
|
||||
// build_prompt generates the final prompt with metadata and file tree
|
||||
fn (wsp HeropromptWorkspace) build_prompt(text string) string {
|
||||
user_instructions := wsp.build_user_instructions(text)
|
||||
file_map := build_file_map(wsp.dirs, '')
|
||||
file_map := wsp.build_file_map()
|
||||
file_contents := wsp.build_file_content()
|
||||
|
||||
// Handle reading the prompt file and parse it
|
||||
prompt := HeropromptTmpPrompt{
|
||||
user_instructions: user_instructions
|
||||
file_map: file_map
|
||||
file_contents: file_contents
|
||||
}
|
||||
|
||||
reprompt := $tmpl('./templates/prompt.template')
|
||||
return reprompt
|
||||
}
|
||||
|
||||
// build_file_map creates a complete file map with base path and metadata
|
||||
fn (wsp HeropromptWorkspace) build_file_map() string {
|
||||
mut file_map := ''
|
||||
if wsp.dirs.len > 0 {
|
||||
// Get the common base path from the first directory
|
||||
base_path := wsp.dirs[0].path.path
|
||||
// Find the parent directory of the base path
|
||||
parent_path := if base_path.contains('/') {
|
||||
base_path.split('/')[..base_path.split('/').len - 1].join('/')
|
||||
} else {
|
||||
base_path
|
||||
}
|
||||
|
||||
// Calculate metadata
|
||||
selected_metadata := wsp.get_selected()
|
||||
mut total_files := 0
|
||||
mut total_content_length := 0
|
||||
mut file_extensions := map[string]int{}
|
||||
|
||||
for dir_meta in selected_metadata.dirs {
|
||||
total_files += dir_meta.selected_files.len
|
||||
for file_meta in dir_meta.selected_files {
|
||||
total_content_length += file_meta.content_length
|
||||
if file_meta.extension.len > 0 {
|
||||
file_extensions[file_meta.extension] = file_extensions[file_meta.extension] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build metadata summary
|
||||
mut extensions_summary := ''
|
||||
for ext, count in file_extensions {
|
||||
if extensions_summary.len > 0 {
|
||||
extensions_summary += ', '
|
||||
}
|
||||
extensions_summary += '${ext}(${count})'
|
||||
}
|
||||
|
||||
// Build header with metadata
|
||||
file_map = '${parent_path}\n'
|
||||
file_map += '# Selected Files: ${total_files} | Total Content: ${total_content_length} chars'
|
||||
if extensions_summary.len > 0 {
|
||||
file_map += ' | Extensions: ${extensions_summary}'
|
||||
}
|
||||
file_map += '\n\n'
|
||||
file_map += build_file_tree(wsp.dirs, '')
|
||||
}
|
||||
|
||||
return file_map
|
||||
}
|
||||
|
||||
@@ -208,12 +375,3 @@ fn generate_random_workspace_name() string {
|
||||
|
||||
return '${adj}_${noun}_${number}'
|
||||
}
|
||||
|
||||
fn get_file_extension(filename string) string {
|
||||
parts := filename.split('.')
|
||||
if parts.len < 2 {
|
||||
// Handle the files with no exe such as Dockerfile, LICENSE
|
||||
return ''
|
||||
}
|
||||
return parts[parts.len - 1]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
module heroprompt
|
||||
|
||||
import freeflowuniverse.herolib.data.paramsparser
|
||||
import freeflowuniverse.herolib.data.encoderhero
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
// your checking & initialization code if needed
|
||||
// TODO: Implement template-based prompt generation
|
||||
fn (mut ws HeropromptWorkspace) heroprompt() !string {
|
||||
// TODO: fill in template based on selection
|
||||
return ''
|
||||
}
|
||||
|
||||
// TODO: Implement tree visualization utilities
|
||||
pub fn get_tree() {}
|
||||
|
||||
// TODO: Implement prompt formatting utilities
|
||||
pub fn format_prompt() {}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<user_instructions>
|
||||
{{text}}
|
||||
</user_instructions>
|
||||
|
||||
<file_map>
|
||||
{{map}}
|
||||
</file_map>
|
||||
|
||||
<file_contents>
|
||||
{{content}}
|
||||
</file_contents>
|
||||
@@ -1 +0,0 @@
|
||||
TODO:...
|
||||
11
lib/develop/heroprompt/templates/prompt.template
Normal file
11
lib/develop/heroprompt/templates/prompt.template
Normal file
@@ -0,0 +1,11 @@
|
||||
<user_instructions>
|
||||
@{prompt.user_instructions}
|
||||
</user_instructions>
|
||||
|
||||
<file_map>
|
||||
@{prompt.file_map}
|
||||
</file_map>
|
||||
|
||||
<file_contents>
|
||||
@{prompt.file_contents}
|
||||
</file_contents>
|
||||
Reference in New Issue
Block a user