From f6c077c6b5edab0ded7d79c176fc614765520b34 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 17 Aug 2025 11:41:29 +0300 Subject: [PATCH] refactor: simplify heroprompt workspace selection model - Introduce HeropromptChild to unify file and dir items - Replace nested Dir/File structs with a flat `children` list - Generate prompt content by traversing the filesystem on-demand - Add `workspace.add_file` for direct file selection - Simplify `workspace.add_dir` to only add the directory path --- .../develop/heroprompt/heroprompt_example.vsh | 57 +- lib/data/encoderhero/decoder.v | 12 +- .../{heroprompt_file.v => heroprompt_child.v} | 23 +- lib/develop/heroprompt/heroprompt_dir.v | 94 -- lib/develop/heroprompt/heroprompt_workspace.v | 1016 +++++++++++------ lib/develop/heroprompt/reprompt_model.v | 4 +- 6 files changed, 678 insertions(+), 528 deletions(-) rename lib/develop/heroprompt/{heroprompt_file.v => heroprompt_child.v} (72%) delete mode 100644 lib/develop/heroprompt/heroprompt_dir.v diff --git a/examples/develop/heroprompt/heroprompt_example.vsh b/examples/develop/heroprompt/heroprompt_example.vsh index de7e241f..8c02e06a 100755 --- a/examples/develop/heroprompt/heroprompt_example.vsh +++ b/examples/develop/heroprompt/heroprompt_example.vsh @@ -1,49 +1,38 @@ #!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run import freeflowuniverse.herolib.develop.heroprompt - import freeflowuniverse.herolib.core.playbook import os - -heroscript_config := ' - !!heropromptworkspace.configure name:"test workspace" path:"${os.home_dir()}/code/github/freeflowuniverse/herolib" -' -mut plbook := playbook.new( - text: heroscript_config -)! - -heroprompt.play(mut plbook)! - -mut workspace1 := heroprompt.new_workspace( - path: '${os.home_dir()}/code/github/freeflowuniverse/herolib' -)! - -// mut workspace2 := heroprompt.get( -// name: 'test workspace' +// heroscript_config := ' +// !!heropromptworkspace.configure name:"test workspace" path:"${os.home_dir()}/code/github/freeflowuniverse/herolib" +// ' +// mut plbook := playbook.new( +// text: heroscript_config // )! -mut dir1 := workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker')! -dir1.select_file(name: 'docker_ubuntu_install.sh')! +// heroprompt.play(mut plbook)! -mut dir2 := workspace1.add_dir( - path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib' +// mut workspace1 := heroprompt.new_workspace( +// path: '${os.home_dir()}/code/github/freeflowuniverse/herolib' +// )! + +mut workspace2 := heroprompt.get( + name: 'test workspace' )! -dir2.select_file(name: '.gitignore')! -dir2.select_file(name: 'build.sh')! -file := dir2.select_file(name: 'debug.sh')! -// println(file.read()!) +// workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker')! +// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/docker_ubuntu_install.sh')! -mut dir3 := workspace1.add_dir( - path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/postgresql' - select_all: true -)! +// workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib')! +// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib/.gitignore')! +// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib/build.sh')! +// workspace1.add_file(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/herolib/debug.sh')! -selected := workspace1.get_selected() +// workspace1.add_dir(path: '${os.home_dir()}/code/github/freeflowuniverse/herolib/docker/postgresql')! -prompt := workspace1.prompt( - text: 'Using the selected files, i want you to get all print statments' -) +// prompt := workspace1.prompt( +// text: 'Using the selected files, i want you to get all print statments' +// ) -println(prompt) +// println(prompt) diff --git a/lib/data/encoderhero/decoder.v b/lib/data/encoderhero/decoder.v index 64f87d9b..94144bda 100644 --- a/lib/data/encoderhero/decoder.v +++ b/lib/data/encoderhero/decoder.v @@ -66,13 +66,11 @@ fn decode_struct[T](_ T, data string) !T { } } } $else $if field.is_array { - $if field.is_array { - $if field.typ is $struct { - mut data_fmt := data.replace(action_str, '') - data_fmt = data.replace('define.${obj_name}', 'define') - typ.$(field.name) = decode_array[field.elem_type](typ.$(field.name), - data_fmt)! - } + if is_struct_array(typ.$(field.name))! { + mut data_fmt := data.replace(action_str, '') + data_fmt = data.replace('define.${obj_name}', 'define') + arr := decode_array(typ.$(field.name), data_fmt)! + typ.$(field.name) = arr } } } diff --git a/lib/develop/heroprompt/heroprompt_file.v b/lib/develop/heroprompt/heroprompt_child.v similarity index 72% rename from lib/develop/heroprompt/heroprompt_file.v rename to lib/develop/heroprompt/heroprompt_child.v index 9dedb7ba..d7b6466b 100644 --- a/lib/develop/heroprompt/heroprompt_file.v +++ b/lib/develop/heroprompt/heroprompt_child.v @@ -1,10 +1,9 @@ module heroprompt -// import freeflowuniverse.herolib.data.paramsparser import freeflowuniverse.herolib.core.pathlib import os -pub struct HeropromptFile { +pub struct HeropromptChild { pub mut: content string path pathlib.Path @@ -33,44 +32,34 @@ pub fn get_file_extension(filename string) string { '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] } // Read the file content -pub fn (fl HeropromptFile) read() !string { - content := os.read_file(fl.path.path)! +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 } diff --git a/lib/develop/heroprompt/heroprompt_dir.v b/lib/develop/heroprompt/heroprompt_dir.v deleted file mode 100644 index 044a5301..00000000 --- a/lib/develop/heroprompt/heroprompt_dir.v +++ /dev/null @@ -1,94 +0,0 @@ -module heroprompt - -import os -import freeflowuniverse.herolib.core.pathlib - -@[heap] -pub struct HeropromptDir { -pub mut: - name string - path pathlib.Path - files []&HeropromptFile @[skip; str: skip] - dirs []&HeropromptDir -} - -// Parameters for adding a file to a directory -@[params] -pub struct AddFileParams { -pub mut: - 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('/') { - full_path = dir.path.path + args.name - } - - if !os.exists(full_path) { - return error('File ${full_path} does not exists') - } - - if !os.is_file(full_path) { - return error('Provided path ${full_path} is not a file') - } - - file_content := os.read_file(full_path)! - - file := &HeropromptFile{ - path: pathlib.Path{ - path: full_path - cat: .file - exist: .yes - } - name: args.name - content: file_content - } - - 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_workspace.v b/lib/develop/heroprompt/heroprompt_workspace.v index 7e2d0040..d76ae7b0 100644 --- a/lib/develop/heroprompt/heroprompt_workspace.v +++ b/lib/develop/heroprompt/heroprompt_workspace.v @@ -37,406 +37,344 @@ fn (wsp HeropromptWorkspace) new(args_ NewWorkspaceParams) !&HeropromptWorkspace return workspace } -// WorkspaceItem represents a file or directory in the workspace tree -pub struct WorkspaceItem { -pub mut: - name string // Item name (file or directory name) - path string // Full path to the item - is_directory bool // True if this is a directory - is_file bool // True if this is a file - size i64 // File size in bytes (0 for directories) - extension string // File extension (empty for directories) - children []WorkspaceItem // Child items (for directories) - is_expanded bool // Whether directory is expanded in UI - is_selected bool // Whether this item is selected for prompts - depth int // Depth level in the tree (0 = root) -} +// // WorkspaceItem represents a file or directory in the workspace tree +// pub struct WorkspaceItem { +// pub mut: +// name string // Item name (file or directory name) +// path string // Full path to the item +// is_directory bool // True if this is a directory +// is_file bool // True if this is a file +// size i64 // File size in bytes (0 for directories) +// extension string // File extension (empty for directories) +// children []WorkspaceItem // Child items (for directories) +// is_expanded bool // Whether directory is expanded in UI +// is_selected bool // Whether this item is selected for prompts +// depth int // Depth level in the tree (0 = root) +// } -// WorkspaceList represents the complete hierarchical listing of a workspace -pub struct WorkspaceList { -pub mut: - root_path string // Root path of the workspace - items []WorkspaceItem // Top-level items in the workspace - total_files int // Total number of files - total_dirs int // Total number of directories -} +// // WorkspaceList represents the complete hierarchical listing of a workspace +// pub struct WorkspaceList { +// pub mut: +// root_path string // Root path of the workspace +// items []WorkspaceItem // Top-level items in the workspace +// total_files int // Total number of files +// total_dirs int // Total number of directories +// } -// list returns the complete hierarchical structure of the workspace -pub fn (wsp HeropromptWorkspace) list() WorkspaceList { - mut result := WorkspaceList{ - root_path: wsp.base_path - } +// // list returns the complete hierarchical structure of the workspace +// pub fn (wsp HeropromptWorkspace) list() WorkspaceList { +// mut result := WorkspaceList{ +// root_path: wsp.base_path +// } - if wsp.base_path.len == 0 || !os.exists(wsp.base_path) { - return result - } +// if wsp.base_path.len == 0 || !os.exists(wsp.base_path) { +// return result +// } - // Build the complete tree structure (ALL files and directories) - result.items = wsp.build_workspace_tree(wsp.base_path, 0) - wsp.calculate_totals(result.items, mut result) +// // Build the complete tree structure (ALL files and directories) +// result.items = wsp.build_workspace_tree(wsp.base_path, 0) +// wsp.calculate_totals(result.items, mut result) - // Mark selected items - wsp.mark_selected_items(mut result.items) +// // Mark selected items +// wsp.mark_selected_items(mut result.items) - return result -} +// return result +// } -// build_workspace_tree recursively builds the workspace tree structure -fn (wsp HeropromptWorkspace) build_workspace_tree(path string, depth int) []WorkspaceItem { - mut items := []WorkspaceItem{} +// // build_workspace_tree recursively builds the workspace tree structure +// fn (wsp HeropromptWorkspace) build_workspace_tree(path string, depth int) []WorkspaceItem { +// mut items := []WorkspaceItem{} - entries := os.ls(path) or { return items } +// entries := os.ls(path) or { return items } - for entry in entries { - full_path := os.join_path(path, entry) +// for entry in entries { +// full_path := os.join_path(path, entry) - if os.is_dir(full_path) { - mut dir_item := WorkspaceItem{ - name: entry - path: full_path - is_directory: true - is_file: false - size: 0 - extension: '' - is_expanded: false - is_selected: false - depth: depth - } +// if os.is_dir(full_path) { +// mut dir_item := WorkspaceItem{ +// name: entry +// path: full_path +// is_directory: true +// is_file: false +// size: 0 +// extension: '' +// is_expanded: false +// is_selected: false +// depth: depth +// } - // Recursively get children - dir_item.children = wsp.build_workspace_tree(full_path, depth + 1) - items << dir_item - } else if os.is_file(full_path) { - file_info := os.stat(full_path) or { continue } - extension := get_file_extension(entry) +// // Recursively get children +// dir_item.children = wsp.build_workspace_tree(full_path, depth + 1) +// items << dir_item +// } else if os.is_file(full_path) { +// file_info := os.stat(full_path) or { continue } +// extension := get_file_extension(entry) - file_item := WorkspaceItem{ - name: entry - path: full_path - is_directory: false - is_file: true - size: file_info.size - extension: extension - children: [] - is_expanded: false - is_selected: false - depth: depth - } - items << file_item - } - } +// file_item := WorkspaceItem{ +// name: entry +// path: full_path +// is_directory: false +// is_file: true +// size: file_info.size +// extension: extension +// children: [] +// is_expanded: false +// is_selected: false +// depth: depth +// } +// items << file_item +// } +// } - // Sort: directories first, then files, both alphabetically - items.sort_with_compare(fn (a &WorkspaceItem, b &WorkspaceItem) int { - if a.is_directory && !b.is_directory { - return -1 - } - if !a.is_directory && b.is_directory { - return 1 - } - if a.name < b.name { - return -1 - } - if a.name > b.name { - return 1 - } - return 0 - }) +// // Sort: directories first, then files, both alphabetically +// items.sort_with_compare(fn (a &WorkspaceItem, b &WorkspaceItem) int { +// if a.is_directory && !b.is_directory { +// return -1 +// } +// if !a.is_directory && b.is_directory { +// return 1 +// } +// if a.name < b.name { +// return -1 +// } +// if a.name > b.name { +// return 1 +// } +// return 0 +// }) - return items -} +// return items +// } -// calculate_totals counts total files and directories in the workspace -fn (wsp HeropromptWorkspace) calculate_totals(items []WorkspaceItem, mut result WorkspaceList) { - for item in items { - if item.is_directory { - result.total_dirs++ - wsp.calculate_totals(item.children, mut result) - } else { - result.total_files++ - } - } -} +// // calculate_totals counts total files and directories in the workspace +// fn (wsp HeropromptWorkspace) calculate_totals(items []WorkspaceItem, mut result WorkspaceList) { +// for item in items { +// if item.is_directory { +// result.total_dirs++ +// wsp.calculate_totals(item.children, mut result) +// } else { +// result.total_files++ +// } +// } +// } -// mark_selected_items marks which items are currently selected for prompts -fn (wsp HeropromptWorkspace) mark_selected_items(mut items []WorkspaceItem) { - for mut item in items { - // Check if this item is selected by comparing paths - item.is_selected = wsp.is_item_selected(item.path) +// // mark_selected_items marks which items are currently selected for prompts +// fn (wsp HeropromptWorkspace) mark_selected_items(mut items []WorkspaceItem) { +// for mut item in items { +// // Check if this item is selected by comparing paths +// item.is_selected = wsp.is_item_selected(item.path) - // Recursively mark children - if item.is_directory && item.children.len > 0 { - wsp.mark_selected_items(mut item.children) - } - } -} +// // Recursively mark children +// if item.is_directory && item.children.len > 0 { +// wsp.mark_selected_items(mut item.children) +// } +// } +// } -// is_item_selected checks if a specific path is selected in the workspace -fn (wsp HeropromptWorkspace) is_item_selected(path string) bool { - for dir in wsp.dirs { - // Check if this directory is selected - if dir.path.path == path { - return true - } - - // Check if any file in this directory is selected - for file in dir.files { - if file.path.path == path { - return true - } - } - - // Recursively check subdirectories - if wsp.is_path_in_selected_dirs(path, dir.dirs) { - return true - } - } - return false -} - -// is_path_in_selected_dirs recursively checks subdirectories for selected items -fn (wsp HeropromptWorkspace) is_path_in_selected_dirs(path string, dirs []&HeropromptDir) bool { - for dir in dirs { - if dir.path.path == path { - return true - } - - for file in dir.files { - if file.path.path == path { - return true - } - } - - if wsp.is_path_in_selected_dirs(path, dir.dirs) { - return true - } - } - return false -} +// // is_item_selected checks if a specific path is selected in the workspace +// fn (wsp HeropromptWorkspace) is_item_selected(path string) bool { +// dirs := wsp.children.filter(fn (item &HeropromptChild) bool { +// return item.path.cat == .dir +// }) +// for dir in dirs { +// if dir.path.path == path { +// return true +// } +// files := dir.children.filter(fn (item &HeropromptChild) bool { +// return item.path.cat == .file +// }) +// for file in files { +// if file.path.path == path { +// return true +// } +// } +// child_dirs := dir.children.filter(fn (item &HeropromptChild) bool { +// return item.path.cat == .dir +// }) +// if wsp.is_path_in_selected_dirs(path, child_dirs) { +// return true +// } +// } +// return false +// } +// Selection API @[params] pub struct AddDirParams { pub mut: - path string @[required] - select_all bool + path string @[required] } -pub fn (mut wsp HeropromptWorkspace) add_dir(args_ AddDirParams) !&HeropromptDir { - if args_.path.len == 0 { +@[params] +pub struct AddFileParams { +pub mut: + path string @[required] +} + +// add a directory to the selection (no recursion stored; recursion is done on-demand) +pub fn (mut wsp HeropromptWorkspace) add_dir(args AddDirParams) !HeropromptChild { + if args.path.len == 0 { return error('The dir path is required') } - - if !os.exists(args_.path) { - return error('The provided path does not exists') + if !os.exists(args.path) || !os.is_dir(args.path) { + return error('Path is not an existing directory: ${args.path}') } - - // Normalize absolute path - abs_path := os.real_path(args_.path) - - parts := abs_path.split(os.path_separator) - dir_name := parts[parts.len - 1] - - mut added_dir := &HeropromptDir{ + abs_path := os.real_path(args.path) + name := os.base(abs_path) + mut ch := HeropromptChild{ path: pathlib.Path{ path: abs_path cat: .dir exist: .yes } - name: dir_name + name: name } + wsp.children << ch + return ch +} - if args_.select_all { - added_dir.select_all_files_and_dirs(abs_path) +// add a file to the selection +pub fn (mut wsp HeropromptWorkspace) add_file(args AddFileParams) !HeropromptChild { + if args.path.len == 0 { + return error('The file path is required') } - - wsp.dirs << added_dir - return added_dir + if !os.exists(args.path) || !os.is_file(args.path) { + return error('Path is not an existing file: ${args.path}') + } + abs_path := os.real_path(args.path) + name := os.base(abs_path) + content := os.read_file(abs_path) or { '' } + mut ch := HeropromptChild{ + path: pathlib.Path{ + path: abs_path + cat: .file + exist: .yes + } + name: name + content: content + } + wsp.children << ch + return ch } -// Metadata structures for selected files and directories -struct SelectedFilesMetadata { - content_length int // File content length in characters - extension string // File extension - name string // File name - path string // Full file path +// Build utilities +fn list_files_recursive(root string) []string { + mut out := []string{} + entries := os.ls(root) or { return out } + for e in entries { + fp := os.join_path(root, e) + if os.is_dir(fp) { + out << list_files_recursive(fp) + } else if os.is_file(fp) { + out << fp + } + } + return out } -struct SelectedDirsMetadata { - name string // Directory name - selected_files []SelectedFilesMetadata // Files in this directory -} - -struct HeropromptWorkspaceGetSelected { -pub mut: - dirs []SelectedDirsMetadata // All directories with their selected files -} - -pub fn (wsp HeropromptWorkspace) get_selected() HeropromptWorkspaceGetSelected { - mut result := HeropromptWorkspaceGetSelected{} - - for dir in wsp.dirs { - mut files := []SelectedFilesMetadata{} - for file in dir.files { - files << SelectedFilesMetadata{ - content_length: file.content.len - extension: get_file_extension(file.name) - name: file.name - path: file.path.path +// build_file_content generates formatted content for all selected files (and all files under selected dirs) +fn (wsp HeropromptWorkspace) build_file_content() string { + mut content := '' + // files selected directly + for ch in wsp.children { + if ch.path.cat == .file { + if content.len > 0 { + content += '\n\n' + } + content += '${ch.path.path}\n' + ext := get_file_extension(ch.name) + if ch.content.len == 0 { + // read on demand + ch_content := os.read_file(ch.path.path) or { '' } + if ch_content.len == 0 { + content += '(Empty file)\n' + } else { + content += '```' + ext + '\n' + ch_content + '\n```' + } + } else { + content += '```' + ext + '\n' + ch.content + '\n```' } } - - result.dirs << SelectedDirsMetadata{ - name: dir.name - selected_files: files + } + // files under selected directories + for ch in wsp.children { + if ch.path.cat == .dir { + for f in list_files_recursive(ch.path.path) { + if content.len > 0 { + content += '\n\n' + } + content += f + '\n' + ext := get_file_extension(os.base(f)) + fc := os.read_file(f) or { '' } + if fc.len == 0 { + content += '(Empty file)\n' + } else { + content += '```' + ext + '\n' + fc + '\n```' + } + } } } - - return result + return content } -pub struct HeropromptWorkspacePrompt { -pub mut: - text string -} - -pub fn (wsp HeropromptWorkspace) prompt(args HeropromptWorkspacePrompt) string { - prompt := wsp.build_prompt(args.text) - return prompt -} - -// Placeholder function for future needs, in case we need to highlight the user_instructions block with some addtional messages -fn (wsp HeropromptWorkspace) build_user_instructions(text string) string { - return text -} - -// build_file_tree creates a tree-like representation of directories and files -fn build_file_tree(dirs []&HeropromptDir, prefix string) string { +// Minimal tree builder for selected directories only; marks files with * +fn build_file_tree_fs(roots []HeropromptChild, prefix string) string { mut out := '' - - for i, dir in dirs { - // Determine the correct tree connector - connector := if i == dirs.len - 1 { '└── ' } else { '├── ' } - - // 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 == total_children - 1 { '└── ' } else { '├── ' } - out += '${child_prefix}${file_connector}${file.name} *\n' + for i, root in roots { + if root.path.cat != .dir { + continue } - - // Recurse into subdirectories - for j, sub_dir in dir.dirs { - sub_connector := if dir.files.len + j == total_children - 1 { + connector := if i == roots.len - 1 { '└── ' } else { '├── ' } + out += '${prefix}${connector}${root.name}\n' + child_prefix := if i == roots.len - 1 { prefix + ' ' } else { prefix + '│ ' } + // list children under root + entries := os.ls(root.path.path) or { []string{} } + // sort: dirs first then files + mut dirs := []string{} + mut files := []string{} + for e in entries { + fp := os.join_path(root.path.path, e) + 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 += '${child_prefix}${sub_connector}${sub_dir.name}\n' - - // Recursive call for subdirectory contents - sub_prefix := if dir.files.len + j == total_children - 1 { + 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 + '│ ' } - - // 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) - } + out += build_file_tree_fs([ + HeropromptChild{ + path: pathlib.Path{ + path: d + cat: .dir + exist: .yes + } + name: os.base(d) + }, + ], sub_prefix) } } - return out } -// build_file_content generates formatted content for all selected files -fn (wsp HeropromptWorkspace) build_file_content() string { - 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 @@ -444,52 +382,54 @@ pub mut: 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 := wsp.build_file_map() - file_contents := wsp.build_file_content() - - prompt := HeropromptTmpPrompt{ - user_instructions: user_instructions - file_map: file_map - file_contents: file_contents - } - - reprompt := $tmpl('./templates/prompt.template') - return reprompt +fn (wsp HeropromptWorkspace) build_user_instructions(text string) string { + return text } // 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 + // roots are selected directories + mut roots := []HeropromptChild{} + mut files_only := []HeropromptChild{} + for ch in wsp.children { + if ch.path.cat == .dir { + roots << ch + } else if ch.path.cat == .file { + files_only << ch + } + } + if roots.len > 0 { + base_path := roots[0].path.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() + // metadata 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 + // files under dirs + for r in roots { + for f in list_files_recursive(r.path.path) { + total_files++ + ext := get_file_extension(os.base(f)) + if ext.len > 0 { + file_extensions[ext] = file_extensions[ext] + 1 } + total_content_length += (os.read_file(f) or { '' }).len } } - - // Build metadata summary + // files only + for fo in files_only { + total_files++ + ext := get_file_extension(fo.name) + if ext.len > 0 { + file_extensions[ext] = file_extensions[ext] + 1 + } + total_content_length += fo.content.len + } mut extensions_summary := '' for ext, count in file_extensions { if extensions_summary.len > 0 { @@ -497,20 +437,348 @@ fn (wsp HeropromptWorkspace) build_file_map() string { } 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, '') + file_map += build_file_tree_fs(roots, '') + // list standalone files as well + for fo in files_only { + file_map += fo.path.path + ' *\n' + } } - return file_map } +pub struct HeropromptWorkspacePrompt { +pub mut: + text string +} + +pub fn (wsp HeropromptWorkspace) prompt(args HeropromptWorkspacePrompt) string { + user_instructions := wsp.build_user_instructions(args.text) + file_map := wsp.build_file_map() + file_contents := wsp.build_file_content() + prompt := HeropromptTmpPrompt{ + user_instructions: user_instructions + file_map: file_map + file_contents: file_contents + } + reprompt := $tmpl('./templates/prompt.template') + return reprompt +} + +// // is_path_in_selected_dirs recursively checks subdirectories for selected items +// fn (wsp HeropromptWorkspace) is_path_in_selected_dirs(path string, dirs []&HeropromptChild) bool { +// for dir in dirs { +// if dir.path.cat != .dir { +// continue +// } +// if dir.path.path == path { +// return true +// } +// files := dir.children.filter(fn (item &HeropromptChild) bool { +// return item.path.cat == .file +// }) +// for file in files { +// if file.path.path == path { +// return true +// } +// } +// child_dirs := dir.children.filter(fn (item &HeropromptChild) bool { +// return item.path.cat == .dir +// }) +// if wsp.is_path_in_selected_dirs(path, child_dirs) { +// return true +// } +// } +// return false +// } + +// @[params] +// pub struct AddDirParams { +// pub mut: +// path string @[required] +// select_all bool +// } + +// pub fn (mut wsp HeropromptWorkspace) add_dir(args_ AddDirParams) !&HeropromptChild { +// if args_.path.len == 0 { +// return error('The dir path is required') +// } +// if !os.exists(args_.path) { +// return error('The provided path does not exists') +// } +// abs_path := os.real_path(args_.path) +// parts := abs_path.split(os.path_separator) +// dir_name := parts[parts.len - 1] +// mut added := &HeropromptChild{ +// path: pathlib.Path{ +// path: abs_path +// cat: .dir +// exist: .yes +// } +// name: dir_name +// } +// if args_.select_all { +// added.select_all_files_and_dirs(abs_path) +// } +// wsp.children << added +// return added +// } + +// // Metadata structures for selected files and directories +// struct SelectedFilesMetadata { +// 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 // Directory name +// selected_files []SelectedFilesMetadata // Files in this directory +// } + +// struct HeropromptWorkspaceGetSelected { +// pub mut: +// dirs []SelectedDirsMetadata // All directories with their selected files +// } + +// pub fn (wsp HeropromptWorkspace) get_selected() HeropromptWorkspaceGetSelected { +// mut result := HeropromptWorkspaceGetSelected{} +// for dir in wsp.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .dir +// }) { +// mut files := []SelectedFilesMetadata{} +// for file in dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .file +// }) { +// files << SelectedFilesMetadata{ +// content_length: file.content.len +// extension: get_file_extension(file.name) +// name: file.name +// path: file.path.path +// } +// } +// result.dirs << SelectedDirsMetadata{ +// name: dir.name +// selected_files: files +// } +// } +// return result +// } + +// pub struct HeropromptWorkspacePrompt { +// pub mut: +// text string +// } + +// pub fn (wsp HeropromptWorkspace) prompt(args HeropromptWorkspacePrompt) string { +// prompt := wsp.build_prompt(args.text) +// return prompt +// } + +// // Placeholder function for future needs, in case we need to highlight the user_instructions block with some addtional messages +// fn (wsp HeropromptWorkspace) build_user_instructions(text string) string { +// return text +// } + +// // build_file_tree creates a tree-like representation of directories and files +// fn build_file_tree(dirs []&HeropromptChild, prefix string) string { +// mut out := '' +// for i, dir in dirs { +// if dir.path.cat != .dir { +// continue +// } +// // Determine the correct tree connector +// connector := if i == dirs.len - 1 { '└── ' } else { '├── ' } +// // Directory name +// out += '${prefix}${connector}${dir.name}\n' +// // Calculate new prefix for children +// child_prefix := if i == dirs.len - 1 { prefix + ' ' } else { prefix + '│ ' } +// // Total children (files + subdirs) +// files := dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .file +// }) +// subdirs := dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .dir +// }) +// total_children := files.len + subdirs.len +// // Files in this directory +// for j, file in files { +// file_connector := if j == total_children - 1 { '└── ' } else { '├── ' } +// out += '${child_prefix}${file_connector}${file.name} *\n' +// } +// // Recurse into subdirectories +// for j, sub_dir in subdirs { +// sub_connector := if files.len + j == total_children - 1 { +// '└── ' +// } else { +// '├── ' +// } +// out += '${child_prefix}${sub_connector}${sub_dir.name}\n' +// sub_prefix := if files.len + j == total_children - 1 { +// child_prefix + ' ' +// } else { +// child_prefix + '│ ' +// } +// // Build content for this subdirectory directly without calling build_file_map again +// sub_files := sub_dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .file +// }) +// sub_subdirs := sub_dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .dir +// }) +// sub_total_children := sub_files.len + sub_subdirs.len +// for k, sub_file in sub_files { +// sub_file_connector := if k == sub_total_children - 1 { +// '└── ' +// } else { +// '├── ' +// } +// out += '${sub_prefix}${sub_file_connector}${sub_file.name} *\n' +// } +// if sub_subdirs.len > 0 { +// out += build_file_tree(sub_subdirs, sub_prefix) +// } +// } +// } +// return out +// } + +// // build_file_content generates formatted content for all selected files +// fn (wsp HeropromptWorkspace) build_file_content() string { +// mut content := '' + +// for dir in wsp.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .dir +// }) { +// for file in dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .file +// }) { +// if content.len > 0 { +// content += '\n\n' +// } +// content += '${file.path.path}\n' +// extension := get_file_extension(file.name) +// if file.content.len == 0 { +// content += '(Empty file)\n' +// } else { +// content += '```${extension}\n' +// content += file.content +// content += '\n```' +// } +// } +// content += wsp.build_dir_file_content(dir.children) +// } + +// return content +// } + +// // build_dir_file_content recursively processes subdirectories +// fn (wsp HeropromptWorkspace) build_dir_file_content(dirs []&HeropromptChild) string { +// mut content := '' +// for dir in dirs { +// if dir.path.cat != .dir { +// continue +// } +// for file in dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .file +// }) { +// if content.len > 0 { +// content += '\n\n' +// } +// content += '${file.path.path}\n' +// extension := get_file_extension(file.name) +// if file.content.len == 0 { +// content += '(Empty file)\n' +// } else { +// content += '```${extension}\n' +// content += file.content +// content += '\n```' +// } +// } +// let_subdirs := dir.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .dir +// }) +// if let_subdirs.len > 0 { +// content += wsp.build_dir_file_content(let_subdirs) +// } +// } +// 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 := wsp.build_file_map() +// file_contents := wsp.build_file_content() + +// 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 := '' +// // Consider only top-level directories as roots +// mut roots := wsp.children.filter(fn (c &HeropromptChild) bool { +// return c.path.cat == .dir +// }) +// if roots.len > 0 { +// base_path := roots[0].path.path +// parent_path := if base_path.contains('/') { +// base_path.split('/')[..base_path.split('/').len - 1].join('/') +// } else { +// base_path +// } +// 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 +// } +// } +// } +// mut extensions_summary := '' +// for ext, count in file_extensions { +// if extensions_summary.len > 0 { +// extensions_summary += ', ' +// } +// extensions_summary += '${ext}(${count})' +// } +// 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(roots, '') +// } +// return file_map +// } + /// Generate a random name for the workspace fn generate_random_workspace_name() string { adjectives := [ diff --git a/lib/develop/heroprompt/reprompt_model.v b/lib/develop/heroprompt/reprompt_model.v index 7d010448..fc197f0f 100644 --- a/lib/develop/heroprompt/reprompt_model.v +++ b/lib/develop/heroprompt/reprompt_model.v @@ -12,8 +12,8 @@ const default = true pub struct HeropromptWorkspace { pub mut: name string = 'default' // Workspace name - base_path string // Base path of the workspace - dirs []&HeropromptDir // List of directories in this workspace + base_path string // Base path of the workspace + children []HeropromptChild // List of directories and files in this workspace } @[params]