module heroprompt import rand import time import os import freeflowuniverse.herolib.core.pathlib / /// Create a new workspace /// If the name is not passed, we will generate a random one fn (wsp Workspace) new(args_ NewWorkspaceParams) !&Workspace { mut args := args_ if args.name.len == 0 { args.name = generate_random_workspace_name() } // Validate and set base path if args.path.len > 0 { if !os.exists(args.path) { return error('Workspace path does not exist: ${args.path}') } if !os.is_dir(args.path) { return error('Workspace path is not a directory: ${args.path}') } } mut workspace := &Workspace{ name: args.name base_path: os.real_path(args.path) } 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) // } // // 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 Workspace) list() WorkspaceList { // mut result := WorkspaceList{ // root_path: wsp.base_path // } // 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) // // Mark selected items // wsp.mark_selected_items(mut result.items) // return result // } // // build_workspace_tree recursively builds the workspace tree structure // fn (wsp Workspace) build_workspace_tree(path string, depth int) []WorkspaceItem { // mut items := []WorkspaceItem{} // entries := os.ls(path) or { return items } // 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 // } // // 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 // } // } // // 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 // } // // calculate_totals counts total files and directories in the workspace // fn (wsp Workspace) 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 Workspace) 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) // } // } // } // // is_item_selected checks if a specific path is selected in the workspace // fn (wsp Workspace) 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] } @[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 Workspace) add_dir(args AddDirParams) !HeropromptChild { if args.path.len == 0 { return error('The dir path is required') } if !os.exists(args.path) || !os.is_dir(args.path) { return error('Path is not an existing directory: ${args.path}') } 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: name } wsp.children << ch return ch } // add a file to the selection pub fn (mut wsp Workspace) add_file(args AddFileParams) !HeropromptChild { if args.path.len == 0 { return error('The file path is required') } 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 } // 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 } // build_file_content generates formatted content for all selected files (and all files under selected dirs) fn (wsp Workspace) 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```' } } } // 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 content } // Minimal tree builder for selected directories only; marks files with * fn build_file_tree_fs(roots []HeropromptChild, prefix string) string { mut out := '' for i, root in roots { if root.path.cat != .dir { continue } 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}${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([ HeropromptChild{ path: pathlib.Path{ path: d cat: .dir exist: .yes } name: os.base(d) }, ], sub_prefix) } } return out } pub struct HeropromptTmpPrompt { pub mut: user_instructions string file_map string file_contents string } fn (wsp Workspace) build_user_instructions(text string) string { return text } // build_file_map creates a complete file map with base path and metadata fn (wsp Workspace) build_file_map() string { mut file_map := '' // 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 } // metadata mut total_files := 0 mut total_content_length := 0 mut file_extensions := map[string]int{} // 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 } } // 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 { 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_fs(roots, '') // list standalone files as well for fo in files_only { file_map += fo.path.path + ' *\n' } } return file_map } pub struct WorkspacePrompt { pub mut: text string } pub fn (wsp Workspace) prompt(args WorkspacePrompt) 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 Workspace) 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 Workspace) 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 WorkspaceGetSelected { // pub mut: // dirs []SelectedDirsMetadata // All directories with their selected files // } // pub fn (wsp Workspace) get_selected() WorkspaceGetSelected { // mut result := WorkspaceGetSelected{} // 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 WorkspacePrompt { // pub mut: // text string // } // pub fn (wsp Workspace) prompt(args WorkspacePrompt) 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 Workspace) 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 Workspace) 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 Workspace) 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 Workspace) 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 Workspace) 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 := [ 'brave', 'bright', 'clever', 'swift', 'noble', 'mighty', 'fearless', 'bold', 'wise', 'epic', 'valiant', 'fierce', 'legendary', 'heroic', 'dynamic', ] nouns := [ 'forge', 'script', 'ocean', 'phoenix', 'atlas', 'quest', 'shield', 'dragon', 'code', 'summit', 'path', 'realm', 'spark', 'anvil', 'saga', ] // Seed randomness with time rand.seed([u32(time.now().unix()), u32(time.now().nanosecond)]) adj := adjectives[rand.intn(adjectives.len) or { 0 }] noun := nouns[rand.intn(nouns.len) or { 0 }] number := rand.intn(100) or { 0 } // 0–99 return '${adj}_${noun}_${number}' }