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:
Mahmoud-Emad
2025-08-14 10:56:05 +03:00
parent 14771ed944
commit a58d72615d
12 changed files with 320 additions and 65 deletions

View File

@@ -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
}
}
}

View 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]
}

View File

@@ -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_)!

View File

@@ -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]
}

View File

@@ -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() {}

View File

@@ -1,11 +0,0 @@
<user_instructions>
{{text}}
</user_instructions>
<file_map>
{{map}}
</file_map>
<file_contents>
{{content}}
</file_contents>

View File

@@ -1 +0,0 @@
TODO:...

View 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>