From a58d72615d054133f5e454fb8bace37f39cc0129 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Thu, 14 Aug 2025 10:56:05 +0300 Subject: [PATCH] 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 --- docker/postgresql/test/Dockerfile | 0 docker/postgresql/test/file.test | 0 .../develop/heroprompt/heroprompt_example.vsh | 8 +- lib/biz/bizmodel/templates/product.md | 13 +- lib/develop/heroprompt/heroprompt_dir.v | 47 +++- lib/develop/heroprompt/heroprompt_file.v | 58 +++++ lib/develop/heroprompt/heroprompt_session.v | 7 +- lib/develop/heroprompt/heroprompt_workspace.v | 220 +++++++++++++++--- lib/develop/heroprompt/reprompt_actions.v | 9 +- .../heroprompt/templates/prompt.heroprompt | 11 - lib/develop/heroprompt/templates/prompt.md | 1 - .../heroprompt/templates/prompt.template | 11 + 12 files changed, 320 insertions(+), 65 deletions(-) create mode 100644 docker/postgresql/test/Dockerfile create mode 100644 docker/postgresql/test/file.test delete mode 100644 lib/develop/heroprompt/templates/prompt.heroprompt delete mode 100644 lib/develop/heroprompt/templates/prompt.md create mode 100644 lib/develop/heroprompt/templates/prompt.template diff --git a/docker/postgresql/test/Dockerfile b/docker/postgresql/test/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/docker/postgresql/test/file.test b/docker/postgresql/test/file.test new file mode 100644 index 00000000..e69de29b diff --git a/examples/develop/heroprompt/heroprompt_example.vsh b/examples/develop/heroprompt/heroprompt_example.vsh index bee22d7c..1b1c2edc 100755 --- a/examples/develop/heroprompt/heroprompt_example.vsh +++ b/examples/develop/heroprompt/heroprompt_example.vsh @@ -20,14 +20,10 @@ dir2.select_file(name: 'build.sh')! dir2.select_file(name: 'debug.sh')! mut dir3 := workspace1.add_dir( - path: '/Users/mahmoud/code/github/freeflowuniverse/herolib/docker/postgresql' - select_all_dirs: true - select_all_files: false - select_all: false + path: '/Users/mahmoud/code/github/freeflowuniverse/herolib/docker/postgresql' + select_all: true )! -dir3.select_file(name: 'docker-compose.yml')! - selected := workspace1.get_selected() prompt := workspace1.prompt( diff --git a/lib/biz/bizmodel/templates/product.md b/lib/biz/bizmodel/templates/product.md index 9789f3b5..9f88638a 100644 --- a/lib/biz/bizmodel/templates/product.md +++ b/lib/biz/bizmodel/templates/product.md @@ -9,7 +9,7 @@ Product ${name1} has revenue events (one offs) -@{model.sheet.wiki() or {''}} +@{model.sheet.wiki() or {''}} namefilter:'${name1}_revenue,${name1}_cogs,${name1}_cogs_perc,${name1}_maintenance_month_perc' sheetname:'bizmodel_tf9 - COGS = Cost of Goods Sold (is our cost to deliver the product/service) @@ -21,14 +21,14 @@ Product ${name1} has revenue events (one offs) Product sold and its revenue/cost of goods -@{model.sheet.wiki() or {''}} +@{model.sheet.wiki() or {''}} namefilter:'${name1}_nr_sold,${name1}_revenue_setup,${name1}_revenue_monthly,${name1}_cogs_setup,${name1}_cogs_setup_perc,${name1}_cogs_monthly,${name1}_cogs_monthly_perc' sheetname:'bizmodel_tf9 - nr sold, is the nr sold per month of ${name1} - revenue setup is setup per item for ${name1}, this is the money we receive. Similar there is a revenue monthly. - cogs = Cost of Goods Sold (is our cost to deliver the product) - - can we as a setup per item, or per month per item + - can we as a setup per item, or per month per item @if product.nr_months_recurring>1 @@ -40,23 +40,22 @@ This product ${name1} is recurring, means customer pays per month ongoing, the p #### the revenue/cogs calculated -@{model.sheet.wiki() or {''}} +@{model.sheet.wiki() or {''}} namefilter:'${name1}_nr_sold_recurring' sheetname:'bizmodel_tf9 This results in following revenues and cogs: -@{model.sheet.wiki() or {''}} +@{model.sheet.wiki() or {''}} namefilter:'${name1}_revenue_setup_total,${name1}_revenue_monthly_total,${name1}_cogs_setup_total,${name1}_cogs_monthly_total,${name1}_cogs_setup_from_perc,${name1}_cogs_monthly_from_perc,${name1}_maintenance_month, ${name1}_revenue_monthly_recurring,${name1}_cogs_monthly_recurring' sheetname:'bizmodel_tf9 resulting revenues: -@{model.sheet.wiki() or {''}} +@{model.sheet.wiki() or {''}} namefilter:'${name1}_revenue_total,${name1}_cogs_total' sheetname:'bizmodel_tf9 - !!!spreadsheet.graph_line_row rowname:'${name1}_cogs_total' unit:million sheetname:'bizmodel_tf9' !!!spreadsheet.graph_line_row rowname:'${name1}_revenue_total' unit:million sheetname:'bizmodel_tf9' diff --git a/lib/develop/heroprompt/heroprompt_dir.v b/lib/develop/heroprompt/heroprompt_dir.v index 0ce43742..88245978 100644 --- a/lib/develop/heroprompt/heroprompt_dir.v +++ b/lib/develop/heroprompt/heroprompt_dir.v @@ -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 + } + } +} diff --git a/lib/develop/heroprompt/heroprompt_file.v b/lib/develop/heroprompt/heroprompt_file.v index b8da43fe..d52f0348 100644 --- a/lib/develop/heroprompt/heroprompt_file.v +++ b/lib/develop/heroprompt/heroprompt_file.v @@ -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] +} diff --git a/lib/develop/heroprompt/heroprompt_session.v b/lib/develop/heroprompt/heroprompt_session.v index 32d4a817..b3c03469 100644 --- a/lib/develop/heroprompt/heroprompt_session.v +++ b/lib/develop/heroprompt/heroprompt_session.v @@ -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_)! diff --git a/lib/develop/heroprompt/heroprompt_workspace.v b/lib/develop/heroprompt/heroprompt_workspace.v index 2e7c5677..8ab92470 100644 --- a/lib/develop/heroprompt/heroprompt_workspace.v +++ b/lib/develop/heroprompt/heroprompt_workspace.v @@ -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] -} diff --git a/lib/develop/heroprompt/reprompt_actions.v b/lib/develop/heroprompt/reprompt_actions.v index 4d4f357b..3e82df0b 100644 --- a/lib/develop/heroprompt/reprompt_actions.v +++ b/lib/develop/heroprompt/reprompt_actions.v @@ -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() {} diff --git a/lib/develop/heroprompt/templates/prompt.heroprompt b/lib/develop/heroprompt/templates/prompt.heroprompt deleted file mode 100644 index 251c06c6..00000000 --- a/lib/develop/heroprompt/templates/prompt.heroprompt +++ /dev/null @@ -1,11 +0,0 @@ - -{{text}} - - - -{{map}} - - - -{{content}} - \ No newline at end of file diff --git a/lib/develop/heroprompt/templates/prompt.md b/lib/develop/heroprompt/templates/prompt.md deleted file mode 100644 index 6fdcc7e2..00000000 --- a/lib/develop/heroprompt/templates/prompt.md +++ /dev/null @@ -1 +0,0 @@ -TODO:... \ No newline at end of file diff --git a/lib/develop/heroprompt/templates/prompt.template b/lib/develop/heroprompt/templates/prompt.template new file mode 100644 index 00000000..f497141a --- /dev/null +++ b/lib/develop/heroprompt/templates/prompt.template @@ -0,0 +1,11 @@ + +@{prompt.user_instructions} + + + +@{prompt.file_map} + + + +@{prompt.file_contents} + \ No newline at end of file