feat: enhance file selection and prompt generation
- Add gitignore filtering to file tree and search - Introduce recursive directory listing API - Enable recursive directory selection in UI - Pass selected paths directly for prompt generation - Refactor API endpoint names and error handling
This commit is contained in:
@@ -9,44 +9,46 @@ import os
|
|||||||
// )!
|
// )!
|
||||||
|
|
||||||
mut workspace := heroprompt.get(
|
mut workspace := heroprompt.get(
|
||||||
name: 'example_wsx'
|
name: ''
|
||||||
path: '${os.home_dir()}/code/github/freeflowuniverse/herolib'
|
path: '${os.home_dir()}/code/github/freeflowuniverse/herolib'
|
||||||
create: true
|
create: true
|
||||||
)!
|
)!
|
||||||
|
|
||||||
// println('workspace (initial): ${workspace}')
|
// println('workspace (initial): ${workspace}')
|
||||||
// println('selected (initial): ${workspace.selected_children()}')
|
// workspace.delete_workspace()!
|
||||||
|
// // println('selected (initial): ${workspace.selected_children()}')
|
||||||
|
// println('workspace (initial): ${workspace}')
|
||||||
|
|
||||||
// Add a directory and a file
|
// // Add a directory and a file
|
||||||
// workspace.add_dir(
|
// // workspace.add_dir(
|
||||||
// path: '${os.home_dir()}/code/github/freeflowuniverse/docusaurus_template/example/docs/howitworks'
|
// // path: '${os.home_dir()}/code/github/freeflowuniverse/docusaurus_template/example/docs/howitworks'
|
||||||
// )!
|
// // )!
|
||||||
// workspace.add_file(
|
// // workspace.add_file(
|
||||||
// path: '${os.home_dir()}/code/github/freeflowuniverse/docusaurus_template/example/docs/howitworks/participants.md'
|
// // path: '${os.home_dir()}/code/github/freeflowuniverse/docusaurus_template/example/docs/howitworks/participants.md'
|
||||||
// )!
|
// // )!
|
||||||
// println('selected (after add): ${workspace.selected_children()}')
|
// // println('selected (after add): ${workspace.selected_children()}')
|
||||||
|
|
||||||
// Build a prompt from current selection (should be empty now)
|
// // Build a prompt from current selection (should be empty now)
|
||||||
mut prompt := workspace.prompt(
|
// mut prompt := workspace.prompt(
|
||||||
text: 'Using the selected files, i want you to get all print statments'
|
// text: 'Using the selected files, i want you to get all print statments'
|
||||||
)
|
// )
|
||||||
|
|
||||||
println('--- PROMPT START ---')
|
// println('--- PROMPT START ---')
|
||||||
println(prompt)
|
// println(prompt)
|
||||||
println('--- PROMPT END ---')
|
// println('--- PROMPT END ---')
|
||||||
|
|
||||||
// // Remove the file by name, then the directory by name
|
// // // Remove the file by name, then the directory by name
|
||||||
// workspace.remove_file(name: 'docker_ubuntu_install.sh') or { println('remove_file: ${err}') }
|
// // workspace.remove_file(name: 'docker_ubuntu_install.sh') or { println('remove_file: ${err}') }
|
||||||
// workspace.remove_dir(name: 'docker') or { println('remove_dir: ${err}') }
|
// // workspace.remove_dir(name: 'docker') or { println('remove_dir: ${err}') }
|
||||||
// println('selected (after remove): ${workspace.selected_children()}')
|
// // println('selected (after remove): ${workspace.selected_children()}')
|
||||||
|
|
||||||
// // List workspaces (names only)
|
// // // List workspaces (names only)
|
||||||
// mut all := heroprompt.list_workspaces() or { []&heroprompt.Workspace{} }
|
// // mut all := heroprompt.list_workspaces() or { []&heroprompt.Workspace{} }
|
||||||
// mut names := []string{}
|
// // mut names := []string{}
|
||||||
// for w in all {
|
// // for w in all {
|
||||||
// names << w.name
|
// // names << w.name
|
||||||
// }
|
// // }
|
||||||
// println('workspaces: ${names}')
|
// // println('workspaces: ${names}')
|
||||||
|
|
||||||
// // Optionally delete the example workspace
|
// // // Optionally delete the example workspace
|
||||||
// workspace.delete_workspace() or { println('delete_workspace: ${err}') }
|
// // workspace.delete_workspace() or { println('delete_workspace: ${err}') }
|
||||||
|
|||||||
@@ -177,6 +177,13 @@ pub fn list_files_recursive(root string) []string {
|
|||||||
|
|
||||||
// build_file_tree_fs builds a file system tree for given root directories
|
// build_file_tree_fs builds a file system tree for given root directories
|
||||||
pub fn build_file_tree_fs(roots []string, prefix string) string {
|
pub fn build_file_tree_fs(roots []string, prefix string) string {
|
||||||
|
// Create ignore matcher with default patterns
|
||||||
|
ignore_matcher := gitignore_matcher_new()
|
||||||
|
return build_file_tree_fs_with_ignore(roots, prefix, &ignore_matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build_file_tree_fs_with_ignore builds a file system tree with ignore pattern filtering
|
||||||
|
pub fn build_file_tree_fs_with_ignore(roots []string, prefix string, ignore_matcher &IgnoreMatcher) string {
|
||||||
mut out := ''
|
mut out := ''
|
||||||
for i, root in roots {
|
for i, root in roots {
|
||||||
if !os.is_dir(root) {
|
if !os.is_dir(root) {
|
||||||
@@ -185,29 +192,58 @@ pub fn build_file_tree_fs(roots []string, prefix string) string {
|
|||||||
connector := if i == roots.len - 1 { '└── ' } else { '├── ' }
|
connector := if i == roots.len - 1 { '└── ' } else { '├── ' }
|
||||||
out += '${prefix}${connector}${os.base(root)}\n'
|
out += '${prefix}${connector}${os.base(root)}\n'
|
||||||
child_prefix := if i == roots.len - 1 { prefix + ' ' } else { prefix + '│ ' }
|
child_prefix := if i == roots.len - 1 { prefix + ' ' } else { prefix + '│ ' }
|
||||||
out += build_file_tree_fs_recursive(root, child_prefix)
|
out += build_file_tree_fs_recursive_with_ignore(root, child_prefix, '', ignore_matcher)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// build_file_tree_fs_recursive builds the contents of a directory without adding the directory name itself
|
// build_file_tree_fs_recursive builds the contents of a directory without adding the directory name itself
|
||||||
fn build_file_tree_fs_recursive(root string, prefix string) string {
|
fn build_file_tree_fs_recursive(root string, prefix string) string {
|
||||||
|
// Create ignore matcher with default patterns for backward compatibility
|
||||||
|
ignore_matcher := gitignore_matcher_new()
|
||||||
|
return build_file_tree_fs_recursive_with_ignore(root, prefix, '', &ignore_matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build_file_tree_fs_recursive_with_ignore builds the contents of a directory with ignore pattern filtering
|
||||||
|
fn build_file_tree_fs_recursive_with_ignore(root string, prefix string, base_rel_path string, ignore_matcher &IgnoreMatcher) string {
|
||||||
mut out := ''
|
mut out := ''
|
||||||
// list children under root
|
// list children under root
|
||||||
entries := os.ls(root) or { []string{} }
|
entries := os.ls(root) or { []string{} }
|
||||||
// sort: dirs first then files
|
// sort: dirs first then files
|
||||||
mut dirs := []string{}
|
mut dirs := []string{}
|
||||||
mut files := []string{}
|
mut files := []string{}
|
||||||
|
|
||||||
for e in entries {
|
for e in entries {
|
||||||
fp := os.join_path(root, e)
|
fp := os.join_path(root, e)
|
||||||
|
|
||||||
|
// Calculate relative path for ignore checking
|
||||||
|
rel_path := if base_rel_path.len > 0 {
|
||||||
|
if base_rel_path.ends_with('/') { base_rel_path + e } else { base_rel_path + '/' + e }
|
||||||
|
} else {
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this entry should be ignored
|
||||||
|
mut should_ignore := ignore_matcher.is_ignored(rel_path)
|
||||||
|
if os.is_dir(fp) && !should_ignore {
|
||||||
|
// Also check directory pattern with trailing slash
|
||||||
|
should_ignore = ignore_matcher.is_ignored(rel_path + '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_ignore {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if os.is_dir(fp) {
|
if os.is_dir(fp) {
|
||||||
dirs << fp
|
dirs << fp
|
||||||
} else if os.is_file(fp) {
|
} else if os.is_file(fp) {
|
||||||
files << fp
|
files << fp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dirs.sort()
|
dirs.sort()
|
||||||
files.sort()
|
files.sort()
|
||||||
|
|
||||||
// files
|
// files
|
||||||
for j, f in files {
|
for j, f in files {
|
||||||
file_connector := if j == files.len - 1 && dirs.len == 0 {
|
file_connector := if j == files.len - 1 && dirs.len == 0 {
|
||||||
@@ -217,6 +253,7 @@ fn build_file_tree_fs_recursive(root string, prefix string) string {
|
|||||||
}
|
}
|
||||||
out += '${prefix}${file_connector}${os.base(f)} *\n'
|
out += '${prefix}${file_connector}${os.base(f)} *\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
// subdirectories
|
// subdirectories
|
||||||
for j, d in dirs {
|
for j, d in dirs {
|
||||||
sub_connector := if j == dirs.len - 1 { '└── ' } else { '├── ' }
|
sub_connector := if j == dirs.len - 1 { '└── ' } else { '├── ' }
|
||||||
@@ -226,7 +263,20 @@ fn build_file_tree_fs_recursive(root string, prefix string) string {
|
|||||||
} else {
|
} else {
|
||||||
prefix + '│ '
|
prefix + '│ '
|
||||||
}
|
}
|
||||||
out += build_file_tree_fs_recursive(d, sub_prefix)
|
|
||||||
|
// Calculate new relative path for subdirectory
|
||||||
|
dir_name := os.base(d)
|
||||||
|
new_rel_path := if base_rel_path.len > 0 {
|
||||||
|
if base_rel_path.ends_with('/') {
|
||||||
|
base_rel_path + dir_name
|
||||||
|
} else {
|
||||||
|
base_rel_path + '/' + dir_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dir_name
|
||||||
|
}
|
||||||
|
|
||||||
|
out += build_file_tree_fs_recursive_with_ignore(d, sub_prefix, new_rel_path, ignore_matcher)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import freeflowuniverse.herolib.develop.codewalker
|
|||||||
@[params]
|
@[params]
|
||||||
pub struct AddDirParams {
|
pub struct AddDirParams {
|
||||||
pub mut:
|
pub mut:
|
||||||
path string @[required]
|
path string @[required]
|
||||||
|
include_tree bool = true // true for base directories, false for selected directories
|
||||||
}
|
}
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
@@ -45,7 +46,7 @@ pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild {
|
|||||||
exist: .yes
|
exist: .yes
|
||||||
}
|
}
|
||||||
name: name
|
name: name
|
||||||
include_tree: true
|
include_tree: args.include_tree
|
||||||
}
|
}
|
||||||
wsp.children << ch
|
wsp.children << ch
|
||||||
wsp.save()!
|
wsp.save()!
|
||||||
@@ -266,6 +267,54 @@ fn (wsp Workspace) build_file_content() !string {
|
|||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build_file_content_for_paths generates formatted content for specific selected paths
|
||||||
|
fn (wsp Workspace) build_file_content_for_paths(selected_paths []string) !string {
|
||||||
|
mut content := ''
|
||||||
|
|
||||||
|
for path in selected_paths {
|
||||||
|
if !os.exists(path) {
|
||||||
|
continue // Skip non-existent paths
|
||||||
|
}
|
||||||
|
|
||||||
|
if content.len > 0 {
|
||||||
|
content += '\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.is_file(path) {
|
||||||
|
// Add file content
|
||||||
|
content += '${path}\n'
|
||||||
|
file_content := os.read_file(path) or {
|
||||||
|
content += '(Error reading file: ${err.msg()})\n'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext := get_file_extension(os.base(path))
|
||||||
|
if file_content.len == 0 {
|
||||||
|
content += '(Empty file)\n'
|
||||||
|
} else {
|
||||||
|
content += '```' + ext + '\n' + file_content + '\n```'
|
||||||
|
}
|
||||||
|
} else if os.is_dir(path) {
|
||||||
|
// Add directory content using codewalker
|
||||||
|
mut cw := codewalker.new(codewalker.CodeWalkerArgs{})!
|
||||||
|
mut fm := cw.filemap_get(path: path)!
|
||||||
|
for filepath, filecontent in fm.content {
|
||||||
|
if content.len > 0 {
|
||||||
|
content += '\n\n'
|
||||||
|
}
|
||||||
|
content += '${path}/${filepath}\n'
|
||||||
|
ext := get_file_extension(filepath)
|
||||||
|
if filecontent.len == 0 {
|
||||||
|
content += '(Empty file)\n'
|
||||||
|
} else {
|
||||||
|
content += '```' + ext + '\n' + filecontent + '\n```'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
pub struct HeropromptTmpPrompt {
|
pub struct HeropromptTmpPrompt {
|
||||||
pub mut:
|
pub mut:
|
||||||
user_instructions string
|
user_instructions string
|
||||||
@@ -277,14 +326,112 @@ fn (wsp Workspace) build_user_instructions(text string) string {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
// build_file_map creates a complete file map with base path and metadata
|
// build_file_map creates a unified tree showing the minimal path structure for all workspace items
|
||||||
fn (wsp Workspace) build_file_map() string {
|
pub fn (wsp Workspace) build_file_map() string {
|
||||||
mut file_map := ''
|
// Collect all paths from workspace children
|
||||||
|
mut all_paths := []string{}
|
||||||
for ch in wsp.children {
|
for ch in wsp.children {
|
||||||
file_map += codewalker.build_file_tree_fs([ch.path.path], '')
|
all_paths << ch.path.path
|
||||||
file_map += '\n'
|
|
||||||
}
|
}
|
||||||
return file_map
|
|
||||||
|
if all_paths.len == 0 {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand directories to include all their contents
|
||||||
|
expanded_paths := expand_directory_paths(all_paths)
|
||||||
|
|
||||||
|
// Find common root path to make the tree relative
|
||||||
|
common_root := find_common_root_path(expanded_paths)
|
||||||
|
|
||||||
|
// Build unified tree using the selected tree function
|
||||||
|
return codewalker.build_selected_tree(expanded_paths, common_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find_common_root_path finds the common root directory for a list of paths
|
||||||
|
fn find_common_root_path(paths []string) string {
|
||||||
|
if paths.len == 0 {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if paths.len == 1 {
|
||||||
|
// For single path, use its parent directory as root
|
||||||
|
return os.dir(paths[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split all paths into components
|
||||||
|
mut path_components := [][]string{}
|
||||||
|
for path in paths {
|
||||||
|
// Normalize path and split into components
|
||||||
|
normalized := os.real_path(path)
|
||||||
|
components := normalized.split(os.path_separator).filter(it.len > 0)
|
||||||
|
path_components << components
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find common prefix
|
||||||
|
mut common_components := []string{}
|
||||||
|
if path_components.len > 0 {
|
||||||
|
// Find minimum length manually
|
||||||
|
mut min_len := path_components[0].len
|
||||||
|
for components in path_components {
|
||||||
|
if components.len < min_len {
|
||||||
|
min_len = components.len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0 .. min_len {
|
||||||
|
component := path_components[0][i]
|
||||||
|
mut all_match := true
|
||||||
|
for j in 1 .. path_components.len {
|
||||||
|
if path_components[j][i] != component {
|
||||||
|
all_match = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if all_match {
|
||||||
|
common_components << component
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build common root path
|
||||||
|
if common_components.len == 0 {
|
||||||
|
return os.path_separator
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.path_separator + common_components.join(os.path_separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expand_directory_paths expands directory paths to include all files and subdirectories
|
||||||
|
fn expand_directory_paths(paths []string) []string {
|
||||||
|
mut expanded := []string{}
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
if !os.exists(path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.is_file(path) {
|
||||||
|
// Add files directly
|
||||||
|
expanded << path
|
||||||
|
} else if os.is_dir(path) {
|
||||||
|
// Expand directories using codewalker to get all files
|
||||||
|
mut cw := codewalker.new(codewalker.CodeWalkerArgs{}) or { continue }
|
||||||
|
mut fm := cw.filemap_get(path: path) or { continue }
|
||||||
|
|
||||||
|
// Add the directory itself
|
||||||
|
expanded << path
|
||||||
|
|
||||||
|
// Add all files in the directory
|
||||||
|
for filepath, _ in fm.content {
|
||||||
|
full_path := os.join_path(path, filepath)
|
||||||
|
expanded << full_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WorkspacePrompt {
|
pub struct WorkspacePrompt {
|
||||||
@@ -305,6 +452,42 @@ pub fn (wsp Workspace) prompt(args WorkspacePrompt) string {
|
|||||||
return reprompt
|
return reprompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct WorkspacePromptWithSelection {
|
||||||
|
pub mut:
|
||||||
|
text string
|
||||||
|
selected_paths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate prompt with specific selected paths instead of using workspace children
|
||||||
|
pub fn (wsp Workspace) prompt_with_selection(args WorkspacePromptWithSelection) !string {
|
||||||
|
user_instructions := wsp.build_user_instructions(args.text)
|
||||||
|
|
||||||
|
// Build file map for selected paths (unified tree)
|
||||||
|
file_map := if args.selected_paths.len > 0 {
|
||||||
|
// Expand directories to include all their contents
|
||||||
|
expanded_paths := expand_directory_paths(args.selected_paths)
|
||||||
|
common_root := find_common_root_path(expanded_paths)
|
||||||
|
codewalker.build_selected_tree(expanded_paths, common_root)
|
||||||
|
} else {
|
||||||
|
// Fallback to workspace file map if no selections
|
||||||
|
wsp.build_file_map()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build file content only for selected paths
|
||||||
|
file_contents := wsp.build_file_content_for_paths(args.selected_paths) or {
|
||||||
|
return error('failed to build file content: ${err.msg()}')
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := HeropromptTmpPrompt{
|
||||||
|
user_instructions: user_instructions
|
||||||
|
file_map: file_map
|
||||||
|
file_contents: file_contents
|
||||||
|
}
|
||||||
|
reprompt := $tmpl('./templates/prompt.template')
|
||||||
|
return reprompt
|
||||||
|
}
|
||||||
|
|
||||||
// Save the workspace
|
// Save the workspace
|
||||||
fn (mut wsp Workspace) save() !&Workspace {
|
fn (mut wsp Workspace) save() !&Workspace {
|
||||||
wsp.updated = time.now()
|
wsp.updated = time.now()
|
||||||
|
|||||||
@@ -4,14 +4,39 @@ import veb
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import freeflowuniverse.herolib.develop.heroprompt as hp
|
import freeflowuniverse.herolib.develop.heroprompt as hp
|
||||||
|
import freeflowuniverse.herolib.develop.codewalker
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types and Structures
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// Types
|
|
||||||
struct DirResp {
|
struct DirResp {
|
||||||
path string
|
path string
|
||||||
items []hp.ListItem
|
items []hp.ListItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility functions
|
struct SearchResult {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
full_path string
|
||||||
|
type_ string @[json: 'type']
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchResponse {
|
||||||
|
query string
|
||||||
|
results []SearchResult
|
||||||
|
count string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RecursiveListResponse {
|
||||||
|
path string
|
||||||
|
children []map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Utility Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
fn expand_home_path(path string) string {
|
fn expand_home_path(path string) string {
|
||||||
if path.starts_with('~') {
|
if path.starts_with('~') {
|
||||||
home := os.home_dir()
|
home := os.home_dir()
|
||||||
@@ -28,20 +53,42 @@ fn json_success() string {
|
|||||||
return '{"ok":true}'
|
return '{"ok":true}'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursive search function
|
fn set_json_content_type(mut ctx Context) {
|
||||||
fn search_directory(dir_path string, base_path string, query_lower string, mut results []map[string]string) {
|
ctx.set_content_type('application/json')
|
||||||
entries := os.ls(dir_path) or { return }
|
}
|
||||||
|
|
||||||
|
fn get_workspace_or_error(name string, mut ctx Context) ?&hp.Workspace {
|
||||||
|
wsp := hp.get(name: name, create: false) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
ctx.text(json_error('workspace not found'))
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
return wsp
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Search Functionality
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
fn search_files_recursive(base_path string, query string) []SearchResult {
|
||||||
|
mut results := []SearchResult{}
|
||||||
|
query_lower := query.to_lower()
|
||||||
|
|
||||||
|
// Create ignore matcher for consistent filtering
|
||||||
|
ignore_matcher := codewalker.gitignore_matcher_new()
|
||||||
|
|
||||||
|
search_directory_with_ignore(base_path, base_path, query_lower, &ignore_matcher, mut
|
||||||
|
results)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_directory_with_ignore(base_path string, current_path string, query_lower string, ignore_matcher &codewalker.IgnoreMatcher, mut results []SearchResult) {
|
||||||
|
entries := os.ls(current_path) or { return }
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
full_path := os.join_path(dir_path, entry)
|
full_path := os.join_path(current_path, entry)
|
||||||
|
|
||||||
// Skip hidden files and common ignore patterns
|
// Calculate relative path for ignore checking
|
||||||
if entry.starts_with('.') || entry == 'node_modules' || entry == 'target'
|
|
||||||
|| entry == 'build' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get relative path from workspace base
|
|
||||||
mut rel_path := full_path
|
mut rel_path := full_path
|
||||||
if full_path.starts_with(base_path) {
|
if full_path.starts_with(base_path) {
|
||||||
rel_path = full_path[base_path.len..]
|
rel_path = full_path[base_path.len..]
|
||||||
@@ -50,261 +97,377 @@ fn search_directory(dir_path string, base_path string, query_lower string, mut r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this entry should be ignored
|
||||||
|
if ignore_matcher.is_ignored(rel_path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Check if filename or path matches search query
|
// Check if filename or path matches search query
|
||||||
if entry.to_lower().contains(query_lower) || rel_path.to_lower().contains(query_lower) {
|
if entry.to_lower().contains(query_lower) || rel_path.to_lower().contains(query_lower) {
|
||||||
results << {
|
results << SearchResult{
|
||||||
'name': entry
|
name: entry
|
||||||
'path': rel_path
|
path: rel_path
|
||||||
'full_path': full_path
|
full_path: full_path
|
||||||
'type': if os.is_dir(full_path) { 'directory' } else { 'file' }
|
type_: if os.is_dir(full_path) { 'directory' } else { 'file' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively search subdirectories
|
// Recursively search subdirectories
|
||||||
if os.is_dir(full_path) {
|
if os.is_dir(full_path) {
|
||||||
search_directory(full_path, base_path, query_lower, mut results)
|
search_directory_with_ignore(base_path, full_path, query_lower, ignore_matcher, mut
|
||||||
|
results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIs
|
// ============================================================================
|
||||||
|
// Workspace Management API Endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// List all workspaces
|
||||||
@['/api/heroprompt/workspaces'; get]
|
@['/api/heroprompt/workspaces'; get]
|
||||||
pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result {
|
pub fn (app &App) api_heroprompt_list_workspaces(mut ctx Context) veb.Result {
|
||||||
mut names := []string{}
|
mut names := []string{}
|
||||||
ws := hp.list_workspaces_fromdb() or { []&hp.Workspace{} }
|
ws := hp.list_workspaces_fromdb() or { []&hp.Workspace{} }
|
||||||
for w in ws {
|
for w in ws {
|
||||||
names << w.name
|
names << w.name
|
||||||
}
|
}
|
||||||
ctx.set_content_type('application/json')
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json.encode(names))
|
return ctx.text(json.encode(names))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new workspace
|
||||||
@['/api/heroprompt/workspaces'; post]
|
@['/api/heroprompt/workspaces'; post]
|
||||||
pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
|
pub fn (app &App) api_heroprompt_create_workspace(mut ctx Context) veb.Result {
|
||||||
name_input := ctx.form['name'] or { '' }
|
name_input := ctx.form['name'] or { '' }
|
||||||
|
|
||||||
// Name is now required
|
// Validate workspace name
|
||||||
mut name := name_input.trim(' \t\n\r')
|
mut name := name_input.trim(' \t\n\r')
|
||||||
if name.len == 0 {
|
if name.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('workspace name is required'))
|
return ctx.text(json_error('workspace name is required'))
|
||||||
}
|
}
|
||||||
|
|
||||||
wsp := hp.get(name: name, create: true) or { return ctx.text(json_error('create failed')) }
|
// Create workspace
|
||||||
ctx.set_content_type('application/json')
|
wsp := hp.get(name: name, create: true) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error('create failed'))
|
||||||
|
}
|
||||||
|
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json.encode({
|
return ctx.text(json.encode({
|
||||||
'name': wsp.name
|
'name': wsp.name
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get workspace details
|
||||||
@['/api/heroprompt/workspaces/:name'; get]
|
@['/api/heroprompt/workspaces/:name'; get]
|
||||||
pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_get_workspace(mut ctx Context, name string) veb.Result {
|
||||||
wsp := hp.get(name: name, create: false) or {
|
wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
return ctx.text(json_error('workspace not found'))
|
|
||||||
}
|
set_json_content_type(mut ctx)
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json.encode({
|
return ctx.text(json.encode({
|
||||||
'name': wsp.name
|
'name': wsp.name
|
||||||
'selected_files': wsp.selected_children().len.str()
|
'selected_files': wsp.selected_children().len.str()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update workspace
|
||||||
@['/api/heroprompt/workspaces/:name'; put]
|
@['/api/heroprompt/workspaces/:name'; put]
|
||||||
pub fn (app &App) api_heroprompt_update(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_update_workspace(mut ctx Context, name string) veb.Result {
|
||||||
wsp := hp.get(name: name, create: false) or {
|
wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
return ctx.text(json_error('workspace not found'))
|
|
||||||
}
|
|
||||||
|
|
||||||
new_name := ctx.form['name'] or { name }
|
new_name := ctx.form['name'] or { name }
|
||||||
|
|
||||||
// Update the workspace using the update_workspace method
|
// Update the workspace
|
||||||
updated_wsp := wsp.update_workspace(
|
updated_wsp := wsp.update_workspace(name: new_name) or {
|
||||||
name: new_name
|
set_json_content_type(mut ctx)
|
||||||
) or { return ctx.text(json_error('failed to update workspace')) }
|
return ctx.text(json_error('failed to update workspace'))
|
||||||
|
}
|
||||||
|
|
||||||
ctx.set_content_type('application/json')
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json.encode({
|
return ctx.text(json.encode({
|
||||||
'name': updated_wsp.name
|
'name': updated_wsp.name
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete endpoint using POST (VEB framework compatibility)
|
// Delete workspace (using POST for VEB framework compatibility)
|
||||||
@['/api/heroprompt/workspaces/:name/delete'; post]
|
@['/api/heroprompt/workspaces/:name/delete'; post]
|
||||||
pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_delete_workspace(mut ctx Context, name string) veb.Result {
|
||||||
wsp := hp.get(name: name, create: false) or {
|
wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
return ctx.text(json_error('workspace not found'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the workspace
|
// Delete the workspace
|
||||||
wsp.delete_workspace() or { return ctx.text(json_error('failed to delete workspace')) }
|
wsp.delete_workspace() or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error('failed to delete workspace'))
|
||||||
|
}
|
||||||
|
|
||||||
ctx.set_content_type('application/json')
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_success())
|
return ctx.text(json_success())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// File and Directory Operations API Endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// List directory contents
|
||||||
@['/api/heroprompt/directory'; get]
|
@['/api/heroprompt/directory'; get]
|
||||||
pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result {
|
pub fn (app &App) api_heroprompt_list_directory(mut ctx Context) veb.Result {
|
||||||
wsname := ctx.query['name'] or { 'default' }
|
wsname := ctx.query['name'] or { 'default' }
|
||||||
path_q := ctx.query['path'] or { '' }
|
path_q := ctx.query['path'] or { '' }
|
||||||
base_path := ctx.query['base'] or { '' }
|
base_path := ctx.query['base'] or { '' }
|
||||||
|
|
||||||
if base_path.len == 0 {
|
if base_path.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('base path is required'))
|
return ctx.text(json_error('base path is required'))
|
||||||
}
|
}
|
||||||
|
|
||||||
mut wsp := hp.get(name: wsname, create: false) or {
|
wsp := get_workspace_or_error(wsname, mut ctx) or { return ctx.text('') }
|
||||||
return ctx.text(json_error('workspace not found'))
|
|
||||||
}
|
|
||||||
items := wsp.list_dir(base_path, path_q) or {
|
items := wsp.list_dir(base_path, path_q) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('cannot list directory'))
|
return ctx.text(json_error('cannot list directory'))
|
||||||
}
|
}
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json.encode(DirResp{
|
return ctx.text(json.encode(DirResp{
|
||||||
path: if path_q.len > 0 { path_q } else { base_path }
|
path: if path_q.len > 0 { path_q } else { base_path }
|
||||||
items: items
|
items: items
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get file content
|
||||||
@['/api/heroprompt/file'; get]
|
@['/api/heroprompt/file'; get]
|
||||||
pub fn (app &App) api_heroprompt_file(mut ctx Context) veb.Result {
|
pub fn (app &App) api_heroprompt_get_file(mut ctx Context) veb.Result {
|
||||||
wsname := ctx.query['name'] or { 'default' }
|
wsname := ctx.query['name'] or { 'default' }
|
||||||
path_q := ctx.query['path'] or { '' }
|
path_q := ctx.query['path'] or { '' }
|
||||||
|
|
||||||
if path_q.len == 0 {
|
if path_q.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('path required'))
|
return ctx.text(json_error('path required'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the path directly (should be absolute)
|
// Validate file exists and is readable
|
||||||
file_path := path_q
|
if !os.is_file(path_q) {
|
||||||
if !os.is_file(file_path) {
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('not a file'))
|
return ctx.text(json_error('not a file'))
|
||||||
}
|
}
|
||||||
content := os.read_file(file_path) or { return ctx.text(json_error('failed to read')) }
|
|
||||||
ctx.set_content_type('application/json')
|
content := os.read_file(path_q) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error('failed to read file'))
|
||||||
|
}
|
||||||
|
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json.encode({
|
return ctx.text(json.encode({
|
||||||
'language': detect_lang(file_path)
|
'language': detect_lang(path_q)
|
||||||
'content': content
|
'content': content
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add file to workspace
|
||||||
@['/api/heroprompt/workspaces/:name/files'; post]
|
@['/api/heroprompt/workspaces/:name/files'; post]
|
||||||
pub fn (app &App) api_heroprompt_add_file(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_add_file(mut ctx Context, name string) veb.Result {
|
||||||
path := ctx.form['path'] or { '' }
|
path := ctx.form['path'] or { '' }
|
||||||
if path.len == 0 {
|
if path.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('path required'))
|
return ctx.text(json_error('path required'))
|
||||||
}
|
}
|
||||||
mut wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text(json_error('workspace not found'))
|
mut wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
|
|
||||||
|
wsp.add_file(path: path) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error(err.msg()))
|
||||||
}
|
}
|
||||||
wsp.add_file(path: path) or { return ctx.text(json_error(err.msg())) }
|
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_success())
|
return ctx.text(json_success())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add directory to workspace
|
||||||
@['/api/heroprompt/workspaces/:name/dirs'; post]
|
@['/api/heroprompt/workspaces/:name/dirs'; post]
|
||||||
pub fn (app &App) api_heroprompt_add_dir(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_add_directory(mut ctx Context, name string) veb.Result {
|
||||||
path := ctx.form['path'] or { '' }
|
path := ctx.form['path'] or { '' }
|
||||||
if path.len == 0 {
|
if path.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('path required'))
|
return ctx.text(json_error('path required'))
|
||||||
}
|
}
|
||||||
mut wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text(json_error('workspace not found'))
|
mut wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
|
|
||||||
|
wsp.add_dir(path: path) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error(err.msg()))
|
||||||
}
|
}
|
||||||
wsp.add_dir(path: path) or { return ctx.text(json_error(err.msg())) }
|
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_success())
|
return ctx.text(json_success())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Prompt Generation and Search API Endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Generate prompt from workspace selection
|
||||||
@['/api/heroprompt/workspaces/:name/prompt'; post]
|
@['/api/heroprompt/workspaces/:name/prompt'; post]
|
||||||
pub fn (app &App) api_heroprompt_generate_prompt(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_generate_prompt(mut ctx Context, name string) veb.Result {
|
||||||
text := ctx.form['text'] or { '' }
|
text := ctx.form['text'] or { '' }
|
||||||
mut wsp := hp.get(name: name, create: false) or {
|
selected_paths_json := ctx.form['selected_paths'] or { '[]' }
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json_error('workspace not found'))
|
wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
|
|
||||||
|
// Parse selected paths
|
||||||
|
selected_paths := json.decode([]string, selected_paths_json) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error('invalid selected paths format'))
|
||||||
}
|
}
|
||||||
prompt := wsp.prompt(text: text)
|
|
||||||
|
// Generate prompt with selected paths
|
||||||
|
prompt := wsp.prompt_with_selection(text: text, selected_paths: selected_paths) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error('failed to generate prompt: ${err.msg()}'))
|
||||||
|
}
|
||||||
|
|
||||||
ctx.set_content_type('text/plain')
|
ctx.set_content_type('text/plain')
|
||||||
return ctx.text(prompt)
|
return ctx.text(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@['/api/heroprompt/workspaces/:name/selection'; post]
|
// Search files in workspace
|
||||||
pub fn (app &App) api_heroprompt_sync_selection(mut ctx Context, name string) veb.Result {
|
|
||||||
paths_json := ctx.form['paths'] or { '[]' }
|
|
||||||
mut wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text(json_error('workspace not found'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear current selection
|
|
||||||
wsp.children.clear()
|
|
||||||
|
|
||||||
// Parse paths and add them to workspace
|
|
||||||
paths := json.decode([]string, paths_json) or {
|
|
||||||
return ctx.text(json_error('invalid paths format'))
|
|
||||||
}
|
|
||||||
|
|
||||||
for path in paths {
|
|
||||||
if os.is_file(path) {
|
|
||||||
wsp.add_file(path: path) or {
|
|
||||||
continue // Skip files that can't be added
|
|
||||||
}
|
|
||||||
} else if os.is_dir(path) {
|
|
||||||
wsp.add_dir(path: path) or {
|
|
||||||
continue // Skip directories that can't be added
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.text(json_success())
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/workspaces/:name/search'; get]
|
@['/api/heroprompt/workspaces/:name/search'; get]
|
||||||
pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_search_files(mut ctx Context, name string) veb.Result {
|
||||||
query := ctx.query['q'] or { '' }
|
query := ctx.query['q'] or { '' }
|
||||||
base_path := ctx.query['base'] or { '' }
|
base_path := ctx.query['base'] or { '' }
|
||||||
|
|
||||||
|
// Validate input parameters
|
||||||
if query.len == 0 {
|
if query.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('search query required'))
|
return ctx.text(json_error('search query required'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if base_path.len == 0 {
|
if base_path.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json_error('base path required for search'))
|
return ctx.text(json_error('base path required for search'))
|
||||||
}
|
}
|
||||||
|
|
||||||
wsp := hp.get(name: name, create: false) or {
|
wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
return ctx.text(json_error('workspace not found'))
|
|
||||||
|
// Perform search using improved search function
|
||||||
|
results := search_files_recursive(base_path, query)
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
response := SearchResponse{
|
||||||
|
query: query
|
||||||
|
results: results
|
||||||
|
count: results.len.str()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple recursive file search implementation
|
set_json_content_type(mut ctx)
|
||||||
mut results := []map[string]string{}
|
return ctx.text(json.encode(response))
|
||||||
query_lower := query.to_lower()
|
|
||||||
|
|
||||||
// Recursive function to search files
|
|
||||||
search_directory(base_path, base_path, query_lower, mut results)
|
|
||||||
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
|
|
||||||
// Manually build JSON response to avoid encoding issues
|
|
||||||
mut json_results := '['
|
|
||||||
for i, result in results {
|
|
||||||
if i > 0 {
|
|
||||||
json_results += ','
|
|
||||||
}
|
|
||||||
json_results += '{'
|
|
||||||
json_results += '"name":"${result['name']}",'
|
|
||||||
json_results += '"path":"${result['path']}",'
|
|
||||||
json_results += '"full_path":"${result['full_path']}",'
|
|
||||||
json_results += '"type":"${result['type']}"'
|
|
||||||
json_results += '}'
|
|
||||||
}
|
|
||||||
json_results += ']'
|
|
||||||
|
|
||||||
response := '{"query":"${query}","results":${json_results},"count":"${results.len}"}'
|
|
||||||
return ctx.text(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get workspace selected children
|
||||||
@['/api/heroprompt/workspaces/:name/children'; get]
|
@['/api/heroprompt/workspaces/:name/children'; get]
|
||||||
pub fn (app &App) api_heroprompt_get_children(mut ctx Context, name string) veb.Result {
|
pub fn (app &App) api_heroprompt_get_workspace_children(mut ctx Context, name string) veb.Result {
|
||||||
wsp := hp.get(name: name, create: false) or {
|
wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
return ctx.text(json_error('workspace not found'))
|
|
||||||
}
|
|
||||||
|
|
||||||
children := wsp.selected_children()
|
children := wsp.selected_children()
|
||||||
ctx.set_content_type('application/json')
|
set_json_content_type(mut ctx)
|
||||||
return ctx.text(json.encode(children))
|
return ctx.text(json.encode(children))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all recursive children of a directory (for directory selection)
|
||||||
|
@['/api/heroprompt/workspaces/:name/list'; get]
|
||||||
|
pub fn (app &App) api_heroprompt_list_directory_recursive(mut ctx Context, name string) veb.Result {
|
||||||
|
path_q := ctx.query['path'] or { '' }
|
||||||
|
if path_q.len == 0 {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error('path parameter is required'))
|
||||||
|
}
|
||||||
|
|
||||||
|
wsp := get_workspace_or_error(name, mut ctx) or { return ctx.text('') }
|
||||||
|
|
||||||
|
// Get all recursive children of the directory
|
||||||
|
children := get_recursive_directory_children(path_q) or {
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json_error('failed to list directory: ${err.msg()}'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
response := RecursiveListResponse{
|
||||||
|
path: path_q
|
||||||
|
children: children
|
||||||
|
}
|
||||||
|
|
||||||
|
set_json_content_type(mut ctx)
|
||||||
|
return ctx.text(json.encode(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Directory Traversal Helper Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Get all recursive children of a directory with proper gitignore filtering
|
||||||
|
fn get_recursive_directory_children(dir_path string) ![]map[string]string {
|
||||||
|
// Validate directory exists
|
||||||
|
if !os.exists(dir_path) {
|
||||||
|
return error('directory does not exist: ${dir_path}')
|
||||||
|
}
|
||||||
|
if !os.is_dir(dir_path) {
|
||||||
|
return error('path is not a directory: ${dir_path}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ignore matcher with default patterns for consistent filtering
|
||||||
|
ignore_matcher := codewalker.gitignore_matcher_new()
|
||||||
|
|
||||||
|
mut results := []map[string]string{}
|
||||||
|
collect_directory_children_recursive(dir_path, dir_path, &ignore_matcher, mut results) or {
|
||||||
|
return error('failed to collect directory children: ${err.msg()}')
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively collect all children with proper gitignore filtering
|
||||||
|
fn collect_directory_children_recursive(base_dir string, current_dir string, ignore_matcher &codewalker.IgnoreMatcher, mut results []map[string]string) ! {
|
||||||
|
entries := os.ls(current_dir) or { return error('cannot list directory: ${current_dir}') }
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
full_path := os.join_path(current_dir, entry)
|
||||||
|
|
||||||
|
// Calculate relative path from base directory for ignore checking
|
||||||
|
rel_path := calculate_relative_path(full_path, base_dir)
|
||||||
|
|
||||||
|
// Check if this entry should be ignored using proper gitignore logic
|
||||||
|
if ignore_matcher.is_ignored(rel_path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this entry to results using full absolute path to match tree format
|
||||||
|
results << {
|
||||||
|
'name': entry
|
||||||
|
'path': full_path
|
||||||
|
'type': if os.is_dir(full_path) { 'directory' } else { 'file' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a directory, recursively collect its children
|
||||||
|
if os.is_dir(full_path) {
|
||||||
|
collect_directory_children_recursive(base_dir, full_path, ignore_matcher, mut
|
||||||
|
results) or {
|
||||||
|
// Continue on error to avoid stopping the entire operation
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate relative path from base directory
|
||||||
|
fn calculate_relative_path(full_path string, base_dir string) string {
|
||||||
|
mut rel_path := full_path
|
||||||
|
if full_path.starts_with(base_dir) {
|
||||||
|
rel_path = full_path[base_dir.len..]
|
||||||
|
if rel_path.starts_with('/') {
|
||||||
|
rel_path = rel_path[1..]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rel_path
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,14 +141,27 @@ class SimpleFileTree {
|
|||||||
checkbox.type = 'checkbox';
|
checkbox.type = 'checkbox';
|
||||||
checkbox.className = 'tree-checkbox';
|
checkbox.className = 'tree-checkbox';
|
||||||
checkbox.checked = selected.has(path);
|
checkbox.checked = selected.has(path);
|
||||||
checkbox.addEventListener('change', (e) => {
|
checkbox.addEventListener('change', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (checkbox.checked) {
|
if (checkbox.checked) {
|
||||||
selected.add(path);
|
selected.add(path);
|
||||||
|
// If this is a directory, also select all its children
|
||||||
|
if (item.type === 'directory') {
|
||||||
|
await this.selectDirectoryChildren(path, true);
|
||||||
|
} else {
|
||||||
|
// For files, update UI immediately since no async operation
|
||||||
|
this.updateSelectionUI();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selected.delete(path);
|
selected.delete(path);
|
||||||
|
// If this is a directory, also deselect all its children
|
||||||
|
if (item.type === 'directory') {
|
||||||
|
await this.selectDirectoryChildren(path, false);
|
||||||
|
} else {
|
||||||
|
// For files, update UI immediately since no async operation
|
||||||
|
this.updateSelectionUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.updateSelectionUI();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expand/collapse button for directories
|
// Expand/collapse button for directories
|
||||||
@@ -218,11 +231,20 @@ class SimpleFileTree {
|
|||||||
// Remove from loaded paths so it can be reloaded when expanded again
|
// Remove from loaded paths so it can be reloaded when expanded again
|
||||||
this.loadedPaths.delete(dirPath);
|
this.loadedPaths.delete(dirPath);
|
||||||
} else {
|
} else {
|
||||||
// Expand
|
// Expand - update UI optimistically but revert on error
|
||||||
this.expandedDirs.add(dirPath);
|
this.expandedDirs.add(dirPath);
|
||||||
if (expandBtn) expandBtn.innerHTML = '▼';
|
if (expandBtn) expandBtn.innerHTML = '▼';
|
||||||
if (icon) icon.textContent = '📂';
|
if (icon) icon.textContent = '📂';
|
||||||
await this.loadChildren(dirPath);
|
|
||||||
|
// Try to load children
|
||||||
|
const success = await this.loadChildren(dirPath);
|
||||||
|
|
||||||
|
// If loading failed, revert the UI state
|
||||||
|
if (!success) {
|
||||||
|
this.expandedDirs.delete(dirPath);
|
||||||
|
if (expandBtn) expandBtn.innerHTML = '▶';
|
||||||
|
if (icon) icon.textContent = '📁';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +278,7 @@ class SimpleFileTree {
|
|||||||
|
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
console.warn('Failed to load directory:', parentPath, r.error);
|
console.warn('Failed to load directory:', parentPath, r.error);
|
||||||
return;
|
return false; // Return false to indicate failure
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort items: directories first, then files
|
// Sort items: directories first, then files
|
||||||
@@ -271,7 +293,7 @@ class SimpleFileTree {
|
|||||||
const parentElement = qs(`[data-path="${parentPath}"]`);
|
const parentElement = qs(`[data-path="${parentPath}"]`);
|
||||||
if (!parentElement) {
|
if (!parentElement) {
|
||||||
console.warn('Parent element not found for path:', parentPath);
|
console.warn('Parent element not found for path:', parentPath);
|
||||||
return;
|
return false; // Return false to indicate failure
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentDepth = parseInt(parentElement.dataset.depth || '0');
|
const parentDepth = parseInt(parentElement.dataset.depth || '0');
|
||||||
@@ -316,6 +338,7 @@ class SimpleFileTree {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.loadedPaths.add(parentPath);
|
this.loadedPaths.add(parentPath);
|
||||||
|
return true; // Return true to indicate success
|
||||||
}
|
}
|
||||||
|
|
||||||
getDepth(path) {
|
getDepth(path) {
|
||||||
@@ -378,6 +401,58 @@ class SimpleFileTree {
|
|||||||
if (tokenCountEl) tokenCountEl.textContent = tokens.toString();
|
if (tokenCountEl) tokenCountEl.textContent = tokens.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select or deselect all children of a directory recursively
|
||||||
|
async selectDirectoryChildren(dirPath, select) {
|
||||||
|
// First, get all children from API to update the selection state
|
||||||
|
await this.selectDirectoryChildrenFromAPI(dirPath, select);
|
||||||
|
|
||||||
|
// Then, update any currently visible children in the DOM
|
||||||
|
this.updateVisibleCheckboxes();
|
||||||
|
|
||||||
|
// Update the selection UI once at the end
|
||||||
|
this.updateSelectionUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all visible checkboxes to match the current selection state
|
||||||
|
updateVisibleCheckboxes() {
|
||||||
|
const treeItems = document.querySelectorAll('.tree-item');
|
||||||
|
|
||||||
|
treeItems.forEach(item => {
|
||||||
|
const itemPath = item.dataset.path;
|
||||||
|
const checkbox = item.querySelector('.tree-checkbox');
|
||||||
|
if (checkbox && itemPath) {
|
||||||
|
// Set checkbox state based on current selection
|
||||||
|
checkbox.checked = selected.has(itemPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select directory children using API to get complete recursive list
|
||||||
|
async selectDirectoryChildrenFromAPI(dirPath, select) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/list?path=${encodeURIComponent(dirPath)}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.children) {
|
||||||
|
data.children.forEach(child => {
|
||||||
|
const childPath = child.path;
|
||||||
|
if (select) {
|
||||||
|
selected.add(childPath);
|
||||||
|
} else {
|
||||||
|
selected.delete(childPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch directory children:', response.status, response.statusText);
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Error response:', errorText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error selecting directory children:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createFileCard(path) {
|
createFileCard(path) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'file-card';
|
card.className = 'file-card';
|
||||||
@@ -1026,19 +1101,15 @@ async function generatePrompt() {
|
|||||||
outputEl.innerHTML = '<div class="loading">Generating prompt...</div>';
|
outputEl.innerHTML = '<div class="loading">Generating prompt...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// sync selection to backend before generating
|
// Pass selections directly to prompt generation
|
||||||
const paths = Array.from(selected);
|
const paths = Array.from(selected);
|
||||||
const syncResult = await post(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/selection`, {
|
const formData = new URLSearchParams();
|
||||||
paths: JSON.stringify(paths)
|
formData.append('text', promptText);
|
||||||
});
|
formData.append('selected_paths', JSON.stringify(paths));
|
||||||
|
|
||||||
if (syncResult.error) {
|
|
||||||
throw new Error(`Failed to sync selection: ${syncResult.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/prompt`, {
|
const r = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/prompt`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new URLSearchParams({ text: promptText })
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user