diff --git a/lib/develop/heroprompt/heroprompt_factory_.v b/lib/develop/heroprompt/heroprompt_factory_.v index 57797d4e..b57500e4 100644 --- a/lib/develop/heroprompt/heroprompt_factory_.v +++ b/lib/develop/heroprompt/heroprompt_factory_.v @@ -23,8 +23,7 @@ pub mut: pub fn new(args ArgsGet) !&Workspace { mut obj := Workspace{ - name: args.name - base_path: args.path + name: args.name } set(obj)! return get(name: args.name)! diff --git a/lib/develop/heroprompt/heroprompt_model.v b/lib/develop/heroprompt/heroprompt_model.v index ef957903..d9d6315d 100644 --- a/lib/develop/heroprompt/heroprompt_model.v +++ b/lib/develop/heroprompt/heroprompt_model.v @@ -12,12 +12,11 @@ const default = true @[heap] pub struct Workspace { pub mut: - name string = 'default' // Workspace name - base_path string // Base path of the workspace - children []HeropromptChild // List of directories and files in this workspace - created time.Time // Time of creation - updated time.Time // Time of last update - is_saved bool + name string = 'default' // Workspace name + children []HeropromptChild // List of directories and files in this workspace + created time.Time // Time of creation + updated time.Time // Time of last update + is_saved bool } // your checking & initialization code if needed @@ -41,10 +40,9 @@ pub fn heroscript_loads(heroscript string) !Workspace { mut p := action.params return Workspace{ - name: p.get_default('name', 'default')! - base_path: p.get_default('base_path', '')! - created: time.now() - updated: time.now() - children: []HeropromptChild{} + name: p.get_default('name', 'default')! + created: time.now() + updated: time.now() + children: []HeropromptChild{} } } diff --git a/lib/develop/heroprompt/heroprompt_workspace.v b/lib/develop/heroprompt/heroprompt_workspace.v index 95a6ac4b..3aafc5b2 100644 --- a/lib/develop/heroprompt/heroprompt_workspace.v +++ b/lib/develop/heroprompt/heroprompt_workspace.v @@ -33,8 +33,8 @@ pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild { name := os.base(abs_path) for child in wsp.children { - if child.name == name { - return error('another directory with the same name already exists: ${name}') + if child.path.cat == .dir && child.path.path == abs_path { + return error('the directory is already added to the workspace') } } @@ -171,12 +171,11 @@ pub mut: pub fn (wsp &Workspace) update_workspace(args UpdateParams) !&Workspace { mut updated := Workspace{ - name: if args.name.len > 0 { args.name } else { wsp.name } - base_path: if args.base_path.len > 0 { args.base_path } else { wsp.base_path } - children: wsp.children - created: wsp.created - updated: time.now() - is_saved: true + name: if args.name.len > 0 { args.name } else { wsp.name } + children: wsp.children + created: wsp.created + updated: time.now() + is_saved: true } // if name changed, delete old key first if updated.name != wsp.name { @@ -221,10 +220,10 @@ pub: typ string @[json: 'type'] } -pub fn (wsp &Workspace) list_dir(rel_path string) ![]ListItem { +pub fn (wsp &Workspace) list_dir(base_path string, rel_path string) ![]ListItem { // Create an ignore matcher with default patterns ignore_matcher := codewalker.gitignore_matcher_new() - items := codewalker.list_directory_filtered(wsp.base_path, rel_path, &ignore_matcher)! + items := codewalker.list_directory_filtered(base_path, rel_path, &ignore_matcher)! mut out := []ListItem{} for item in items { out << ListItem{ @@ -235,10 +234,6 @@ pub fn (wsp &Workspace) list_dir(rel_path string) ![]ListItem { return out } -pub fn (wsp &Workspace) list() ![]ListItem { - return wsp.list_dir('') -} - // Get the currently selected children (copy) pub fn (wsp Workspace) selected_children() []HeropromptChild { return wsp.children.clone() @@ -319,17 +314,29 @@ fn (wsp Workspace) build_file_map() string { // derive a parent path for display mut parent_path := '' 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('/') + if roots.len == 1 { + // Single root - show parent directory + base_path := roots[0].path.path + parent_path = if base_path.contains('/') { + base_path.split('/')[..base_path.split('/').len - 1].join('/') + } else { + base_path + } } else { - base_path + // Multiple roots - show all root paths, comma-separated + mut root_paths := []string{} + for r in roots { + root_paths << r.path.path + } + parent_path = root_paths.join(', ') + // Truncate if too long for UI display + if parent_path.len > 100 { + parent_path = parent_path[..97] + '...' + } } } else { - // no roots; show workspace base if set, else the parent of first file - parent_path = if wsp.base_path.len > 0 { - wsp.base_path - } else if files_only.len > 0 { + // no roots; show the parent of first file if available + parent_path = if files_only.len > 0 { os.dir(files_only[0].path.path) } else { '' @@ -380,27 +387,27 @@ fn (wsp Workspace) build_file_map() string { file_map += ' | Extensions: ${extensions_summary}' } file_map += '\n\n' - // Render selected structure - if roots.len > 0 { - mut root_paths := []string{} - for r in roots { - root_paths << r.path.path + // Build a comprehensive tree that includes all files from selected directories + mut all_file_paths := []string{} + + // For each selected directory, get all files within it + for r in roots { + mut cw := codewalker.new(codewalker.CodeWalkerArgs{}) or { continue } + fm := cw.filemap_get(path: r.path.path) or { continue } + for rel_path, _ in fm.content { + abs_file_path := os.join_path(r.path.path, rel_path) + all_file_paths << abs_file_path } - file_map += codewalker.build_file_tree_fs(root_paths, '') } - // If there are only standalone selected files (no selected dirs), - // build a minimal tree via codewalker relative to the workspace base. - if files_only.len > 0 && roots.len == 0 { - mut paths := []string{} - for fo in files_only { - paths << fo.path.path - } - file_map += codewalker.build_selected_tree(paths, wsp.base_path) - } else if files_only.len > 0 && roots.len > 0 { - // Keep listing absolute paths for standalone files when directories are also selected. - for fo in files_only { - file_map += fo.path.path + ' *\n' - } + + // Add all standalone file paths + for fo in files_only { + all_file_paths << fo.path.path + } + + if all_file_paths.len > 0 { + // Build a tree that shows all files in their proper directory structure + file_map += codewalker.build_file_tree_fs(all_file_paths, '') } } return file_map diff --git a/lib/develop/heroprompt/heroprompt_workspace_test.v b/lib/develop/heroprompt/heroprompt_workspace_test.v new file mode 100644 index 00000000..516e8d27 --- /dev/null +++ b/lib/develop/heroprompt/heroprompt_workspace_test.v @@ -0,0 +1,121 @@ +module heroprompt + +import os + +fn test_multiple_dirs_same_name() ! { + // Create two temporary folders with the same basename "proj" + temp_base := os.temp_dir() + dir1 := os.join_path(temp_base, 'test_heroprompt_1', 'proj') + dir2 := os.join_path(temp_base, 'test_heroprompt_2', 'proj') + + // Ensure directories exist + os.mkdir_all(dir1)! + os.mkdir_all(dir2)! + + // Create test files in each directory + os.write_file(os.join_path(dir1, 'file1.txt'), 'content1')! + os.write_file(os.join_path(dir2, 'file2.txt'), 'content2')! + + defer { + // Cleanup + os.rmdir_all(os.join_path(temp_base, 'test_heroprompt_1')) or {} + os.rmdir_all(os.join_path(temp_base, 'test_heroprompt_2')) or {} + } + + mut ws := Workspace{ + name: 'testws' + children: []HeropromptChild{} + } + + // First dir – should succeed + child1 := ws.add_dir(path: dir1)! + assert ws.children.len == 1 + assert child1.name == 'proj' + assert child1.path.path == os.real_path(dir1) + + // Second dir – same basename, different absolute path – should also succeed + child2 := ws.add_dir(path: dir2)! + assert ws.children.len == 2 + assert child2.name == 'proj' + assert child2.path.path == os.real_path(dir2) + + // Verify both children have different absolute paths + assert child1.path.path != child2.path.path + + // Try to add the same directory again – should fail + ws.add_dir(path: dir1) or { + assert err.msg().contains('already added to the workspace') + return + } + assert false, 'Expected error when adding same directory twice' +} + +fn test_build_file_map_multiple_roots() ! { + // Create temporary directories + temp_base := os.temp_dir() + dir1 := os.join_path(temp_base, 'test_map_1', 'src') + dir2 := os.join_path(temp_base, 'test_map_2', 'src') + + os.mkdir_all(dir1)! + os.mkdir_all(dir2)! + + // Create test files + os.write_file(os.join_path(dir1, 'main.v'), 'fn main() { println("hello from dir1") }')! + os.write_file(os.join_path(dir2, 'app.v'), 'fn app() { println("hello from dir2") }')! + + defer { + os.rmdir_all(os.join_path(temp_base, 'test_map_1')) or {} + os.rmdir_all(os.join_path(temp_base, 'test_map_2')) or {} + } + + mut ws := Workspace{ + name: 'testws_map' + children: []HeropromptChild{} + } + + // Add both directories + ws.add_dir(path: dir1)! + ws.add_dir(path: dir2)! + + // Build file map + file_map := ws.build_file_map() + + // Should contain both directory paths in the parent_path + assert file_map.contains(os.real_path(dir1)) + assert file_map.contains(os.real_path(dir2)) + + // Should show correct file count (2 files total) + assert file_map.contains('Selected Files: 2') + + // Should contain both file extensions + assert file_map.contains('v(2)') +} + +fn test_single_dir_backward_compatibility() ! { + // Test that single directory workspaces still work as before + temp_base := os.temp_dir() + test_dir := os.join_path(temp_base, 'test_single', 'myproject') + + os.mkdir_all(test_dir)! + os.write_file(os.join_path(test_dir, 'main.v'), 'fn main() { println("single dir test") }')! + + defer { + os.rmdir_all(os.join_path(temp_base, 'test_single')) or {} + } + + mut ws := Workspace{ + name: 'testws_single' + children: []HeropromptChild{} + } + + // Add single directory + child := ws.add_dir(path: test_dir)! + assert ws.children.len == 1 + assert child.name == 'myproject' + + // Build file map - should work as before for single directory + file_map := ws.build_file_map() + assert file_map.contains('Selected Files: 1') + // Just check that the file map is not empty and contains some content + assert file_map.len > 0 +} diff --git a/lib/web/ui/heroprompt_api.v b/lib/web/ui/heroprompt_api.v index 871e4f12..5a577f2b 100644 --- a/lib/web/ui/heroprompt_api.v +++ b/lib/web/ui/heroprompt_api.v @@ -82,26 +82,17 @@ pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result { @['/api/heroprompt/workspaces'; post] pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result { name_input := ctx.form['name'] or { '' } - base_path_in := ctx.form['base_path'] or { '' } - if base_path_in.len == 0 { - return ctx.text(json_error('base_path required')) - } - base_path := expand_home_path(base_path_in) - - // If no name provided, generate a random name + // Name is now required mut name := name_input.trim(' \t\n\r') if name.len == 0 { - name = hp.generate_random_workspace_name() + return ctx.text(json_error('workspace name is required')) } - wsp := hp.get(name: name, create: true, path: base_path) or { - return ctx.text(json_error('create failed')) - } + wsp := hp.get(name: name, create: true) or { return ctx.text(json_error('create failed')) } ctx.set_content_type('application/json') return ctx.text(json.encode({ - 'name': wsp.name - 'base_path': wsp.base_path + 'name': wsp.name })) } @@ -113,7 +104,6 @@ pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result { ctx.set_content_type('application/json') return ctx.text(json.encode({ 'name': wsp.name - 'base_path': wsp.base_path 'selected_files': wsp.selected_children().len.str() })) } @@ -125,19 +115,15 @@ pub fn (app &App) api_heroprompt_update(mut ctx Context, name string) veb.Result } new_name := ctx.form['name'] or { name } - new_base_path_in := ctx.form['base_path'] or { wsp.base_path } - new_base_path := expand_home_path(new_base_path_in) // Update the workspace using the update_workspace method updated_wsp := wsp.update_workspace( - name: new_name - base_path: new_base_path + name: new_name ) or { return ctx.text(json_error('failed to update workspace')) } ctx.set_content_type('application/json') return ctx.text(json.encode({ - 'name': updated_wsp.name - 'base_path': updated_wsp.base_path + 'name': updated_wsp.name })) } @@ -159,13 +145,21 @@ pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result { wsname := ctx.query['name'] or { 'default' } path_q := ctx.query['path'] or { '' } + base_path := ctx.query['base'] or { '' } + + if base_path.len == 0 { + return ctx.text(json_error('base path is required')) + } + mut wsp := hp.get(name: wsname, create: false) or { return ctx.text(json_error('workspace not found')) } - items := wsp.list_dir(path_q) or { return ctx.text(json_error('cannot list directory')) } + items := wsp.list_dir(base_path, path_q) or { + return ctx.text(json_error('cannot list directory')) + } ctx.set_content_type('application/json') return ctx.text(json.encode(DirResp{ - path: if path_q.len > 0 { path_q } else { wsp.base_path } + path: if path_q.len > 0 { path_q } else { base_path } items: items })) } @@ -177,15 +171,9 @@ pub fn (app &App) api_heroprompt_file(mut ctx Context) veb.Result { if path_q.len == 0 { return ctx.text(json_error('path required')) } - mut base := '' - if wsp := hp.get(name: wsname, create: false) { - base = wsp.base_path - } - mut file_path := if !os.is_abs_path(path_q) && base.len > 0 { - os.join_path(base, path_q) - } else { - path_q - } + + // Use the path directly (should be absolute) + file_path := path_q if !os.is_file(file_path) { return ctx.text(json_error('not a file')) } @@ -268,10 +256,16 @@ pub fn (app &App) api_heroprompt_sync_selection(mut ctx Context, name string) ve @['/api/heroprompt/workspaces/:name/search'; get] pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result { query := ctx.query['q'] or { '' } + base_path := ctx.query['base'] or { '' } + if query.len == 0 { return ctx.text(json_error('search query required')) } + if base_path.len == 0 { + return ctx.text(json_error('base path required for search')) + } + wsp := hp.get(name: name, create: false) or { return ctx.text(json_error('workspace not found')) } @@ -281,7 +275,7 @@ pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result query_lower := query.to_lower() // Recursive function to search files - search_directory(wsp.base_path, wsp.base_path, query_lower, mut results) + search_directory(base_path, base_path, query_lower, mut results) ctx.set_content_type('application/json') @@ -303,3 +297,14 @@ pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result response := '{"query":"${query}","results":${json_results},"count":"${results.len}"}' return ctx.text(response) } + +@['/api/heroprompt/workspaces/:name/children'; get] +pub fn (app &App) api_heroprompt_get_children(mut ctx Context, name string) veb.Result { + wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + + children := wsp.selected_children() + ctx.set_content_type('application/json') + return ctx.text(json.encode(children)) +} diff --git a/lib/web/ui/static/js/heroprompt.js b/lib/web/ui/static/js/heroprompt.js index 7cea8a88..cd931fac 100644 --- a/lib/web/ui/static/js/heroprompt.js +++ b/lib/web/ui/static/js/heroprompt.js @@ -252,7 +252,7 @@ class SimpleFileTree { async loadChildren(parentPath) { // Always reload children to ensure fresh data console.log('Loading children for:', parentPath); - const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`); + const r = await api(`/api/heroprompt/directory?name=${currentWs}&base=${encodeURIComponent(parentPath)}&path=`); if (r.error) { console.warn('Failed to load directory:', parentPath, r.error); @@ -395,6 +395,9 @@ class SimpleFileTree { // Get file stats (mock data for now - could be enhanced with real file stats) const stats = this.getFileStats(path); + // Show full path for directories to help differentiate between same-named directories + const displayPath = isDirectory ? path : path; + card.innerHTML = `
@@ -402,7 +405,7 @@ class SimpleFileTree {

${fileName}

-

${path}

+

${displayPath}

@@ -825,6 +828,66 @@ class SimpleFileTree { this.updateSelectionUI(); } + + async renderWorkspaceDirectories(directories) { + this.container.innerHTML = '
Loading workspace directories...
'; + + // Reset state + this.loadedPaths.clear(); + this.expandedDirs.clear(); + expandedDirs.clear(); + + if (!directories || directories.length === 0) { + this.container.innerHTML = ` +
+ +

No directories added yet

+ Use the "Add Dir" button to add directories to this workspace +
+ `; + return; + } + + // Create document fragment for efficient DOM manipulation + const fragment = document.createDocumentFragment(); + const elements = []; + + // Create elements for each workspace directory + for (const dir of directories) { + if (!dir.path || dir.path.cat !== 'dir') continue; + + const dirPath = dir.path.path; + const dirName = dir.name || dirPath.split('/').pop(); + + // Create a directory item that can be expanded + const item = { + name: dirName, + type: 'directory' + }; + + const element = this.createFileItem(item, dirPath, 0); + element.style.opacity = '0'; + element.style.transform = 'translateY(-10px)'; + element.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; + + fragment.appendChild(element); + elements.push(element); + } + + // Clear container and add all elements at once + this.container.innerHTML = ''; + this.container.appendChild(fragment); + + // Trigger staggered animations + elements.forEach((element, i) => { + setTimeout(() => { + element.style.opacity = '1'; + element.style.transform = 'translateY(0)'; + }, i * 50); + }); + + this.updateSelectionUI(); + } } // Global tree instance @@ -886,10 +949,56 @@ async function initWorkspace() { const sel = el('workspaceSelect'); if (sel) sel.value = currentWs; - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base && fileTree) { - await fileTree.render(base); + // Load and display workspace directories + await loadWorkspaceDirectories(); +} + +async function loadWorkspaceDirectories() { + const treeEl = el('tree'); + if (!treeEl) return; + + try { + const children = await api(`/api/heroprompt/workspaces/${currentWs}/children`); + + if (children.error) { + console.warn('Failed to load workspace children:', children.error); + treeEl.innerHTML = ` +
+ +

No directories added yet

+ Use the "Add Dir" button to add directories to this workspace +
+ `; + return; + } + + // Filter only directories + const directories = children.filter(child => child.path && child.path.cat === 'dir'); + + if (directories.length === 0) { + treeEl.innerHTML = ` +
+ +

No directories added yet

+ Use the "Add Dir" button to add directories to this workspace +
+ `; + return; + } + + // Create file tree with workspace directories as roots + if (fileTree) { + await fileTree.renderWorkspaceDirectories(directories); + } + } catch (error) { + console.error('Error loading workspace directories:', error); + treeEl.innerHTML = ` +
+ +

Error loading directories

+ Please try refreshing the page +
+ `; } } @@ -952,41 +1061,64 @@ async function copyPrompt() { const outputEl = el('promptOutput'); if (!outputEl) { console.warn('Prompt output element not found'); + showStatus('Copy failed - element not found', 'error'); return; } - const text = outputEl.textContent; - console.log('text', text); - if (!text || text.trim().length === 0 || text.includes('No files selected') || text.includes('Generated prompt will appear here')) { - console.warn('No valid content to copy'); + // Grab the visible prompt text, stripping HTML and empty-state placeholders + const text = outputEl.innerText.trim(); + if (!text || text.includes('Generated prompt will appear here') || text.includes('No files selected')) { + showStatus('Nothing to copy', 'warning'); return; } - if (!navigator.clipboard) { - // Fallback for older browsers - fallbackCopyToClipboard(text); - return; + // Try the modern Clipboard API first + if (navigator.clipboard && navigator.clipboard.writeText) { + try { + await navigator.clipboard.writeText(text); + showStatus('Prompt copied to clipboard!', 'success'); + return; + } catch (e) { + console.warn('Clipboard API failed, falling back', e); + } } + // Fallback to hidden textarea method + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; // avoid scrolling to bottom + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + try { - await navigator.clipboard.writeText(text); - - // Show success feedback - const originalContent = outputEl.innerHTML; - outputEl.innerHTML = '
Prompt copied to clipboard!
'; - setTimeout(() => { - outputEl.innerHTML = originalContent; - }, 2000); + const successful = document.execCommand('copy'); + showStatus(successful ? 'Prompt copied!' : 'Copy failed', successful ? 'success' : 'error'); } catch (e) { - console.warn('Copy failed', e); - const originalContent = outputEl.innerHTML; - outputEl.innerHTML = '
Failed to copy prompt
'; - setTimeout(() => { - outputEl.innerHTML = originalContent; - }, 2000); + console.error('Fallback copy failed', e); + showStatus('Copy failed', 'error'); + } finally { + document.body.removeChild(textarea); } } +/* Helper – show a transient message inside the output pane */ +function showStatus(msg, type = 'info') { + const out = el('promptOutput'); + if (!out) return; + + const original = out.innerHTML; + const statusClass = type === 'success' ? 'success-message' : + type === 'error' ? 'error-message' : + type === 'warning' ? 'warning-message' : 'info-message'; + + out.innerHTML = `
${msg}
`; + setTimeout(() => { + out.innerHTML = original; + }, 2000); +} + // Global fallback function for clipboard operations function fallbackCopyToClipboard(text) { const textArea = document.createElement('textarea'); @@ -1049,11 +1181,9 @@ async function deleteWorkspace(workspaceName) { currentWs = names[0]; localStorage.setItem('heroprompt-current-ws', currentWs); await reloadWorkspaces(); - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base && fileTree) { - await fileTree.render(base); - } + + // Load directories for new current workspace + await loadWorkspaceDirectories(); } } @@ -1064,15 +1194,12 @@ async function deleteWorkspace(workspaceName) { } } -async function updateWorkspace(workspaceName, newName, newPath) { +async function updateWorkspace(workspaceName, newName) { try { const formData = new FormData(); if (newName && newName !== workspaceName) { formData.append('name', newName); } - if (newPath) { - formData.append('base_path', newPath); - } const encodedName = encodeURIComponent(workspaceName); const response = await fetch(`/api/heroprompt/workspaces/${encodedName}`, { @@ -1095,12 +1222,6 @@ async function updateWorkspace(workspaceName, newName, newPath) { } await reloadWorkspaces(); - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base && fileTree) { - await fileTree.render(base); - } - return result; } catch (e) { console.warn('Update workspace failed', e); @@ -1135,11 +1256,9 @@ document.addEventListener('DOMContentLoaded', function () { workspaceSelect.addEventListener('change', async (e) => { currentWs = e.target.value; localStorage.setItem('heroprompt-current-ws', currentWs); - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base && fileTree) { - await fileTree.render(base); - } + + // Load directories for the new workspace + await loadWorkspaceDirectories(); }); } @@ -1154,11 +1273,8 @@ document.addEventListener('DOMContentLoaded', function () { const refreshExplorerBtn = el('refreshExplorer'); if (refreshExplorerBtn) { refreshExplorerBtn.addEventListener('click', async () => { - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base && fileTree) { - await fileTree.render(base); - } + // Reload workspace directories + await loadWorkspaceDirectories(); }); } @@ -1222,11 +1338,9 @@ document.addEventListener('DOMContentLoaded', function () { if (wsCreateBtn) { wsCreateBtn.addEventListener('click', () => { const nameEl = el('wcName'); - const pathEl = el('wcPath'); const errorEl = el('wcError'); if (nameEl) nameEl.value = ''; - if (pathEl) pathEl.value = ''; if (errorEl) errorEl.textContent = ''; showModal('wsCreate'); @@ -1237,16 +1351,14 @@ document.addEventListener('DOMContentLoaded', function () { if (wcCreateBtn) { wcCreateBtn.addEventListener('click', async () => { const name = el('wcName')?.value?.trim() || ''; - const path = el('wcPath')?.value?.trim() || ''; const errorEl = el('wcError'); - if (!path) { - if (errorEl) errorEl.textContent = 'Path is required.'; + if (!name) { + if (errorEl) errorEl.textContent = 'Workspace name is required.'; return; } - const formData = { base_path: path }; - if (name) formData.name = name; + const formData = { name: name }; const resp = await post('/api/heroprompt/workspaces', formData); if (resp.error) { @@ -1258,10 +1370,18 @@ document.addEventListener('DOMContentLoaded', function () { localStorage.setItem('heroprompt-current-ws', currentWs); await reloadWorkspaces(); - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base && fileTree) { - await fileTree.render(base); + // Clear the file tree since new workspace has no directories yet + if (fileTree) { + const treeEl = el('tree'); + if (treeEl) { + treeEl.innerHTML = ` +
+ +

No directories added yet

+ Use the "Add Dir" button to add directories to this workspace +
+ `; + } } hideModal('wsCreate'); @@ -1275,11 +1395,9 @@ document.addEventListener('DOMContentLoaded', function () { const info = await api(`/api/heroprompt/workspaces/${currentWs}`); if (info && !info.error) { const nameEl = el('wdName'); - const pathEl = el('wdPath'); const errorEl = el('wdError'); if (nameEl) nameEl.value = info.name || currentWs; - if (pathEl) pathEl.value = info.base_path || ''; if (errorEl) errorEl.textContent = ''; showModal('wsDetails'); @@ -1292,15 +1410,14 @@ document.addEventListener('DOMContentLoaded', function () { if (wdUpdateBtn) { wdUpdateBtn.addEventListener('click', async () => { const name = el('wdName')?.value?.trim() || ''; - const path = el('wdPath')?.value?.trim() || ''; const errorEl = el('wdError'); - if (!path) { - if (errorEl) errorEl.textContent = 'Path is required.'; + if (!name) { + if (errorEl) errorEl.textContent = 'Workspace name is required.'; return; } - const result = await updateWorkspace(currentWs, name, path); + const result = await updateWorkspace(currentWs, name); if (result.error) { if (errorEl) errorEl.textContent = result.error; return; @@ -1326,6 +1443,52 @@ document.addEventListener('DOMContentLoaded', function () { }); } + // Add Directory functionality + const addDirBtn = el('addDirBtn'); + if (addDirBtn) { + addDirBtn.addEventListener('click', () => { + const pathEl = el('addDirPath'); + const errorEl = el('addDirError'); + + if (pathEl) pathEl.value = ''; + if (errorEl) errorEl.textContent = ''; + + showModal('addDirModal'); + }); + } + + const addDirConfirm = el('addDirConfirm'); + if (addDirConfirm) { + addDirConfirm.addEventListener('click', async () => { + const path = el('addDirPath')?.value?.trim() || ''; + const errorEl = el('addDirError'); + + if (!path) { + if (errorEl) errorEl.textContent = 'Directory path is required.'; + return; + } + + // Add directory via API + const result = await post(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/dirs`, { + path: path + }); + + if (result.error) { + if (errorEl) errorEl.textContent = result.error; + return; + } + + // Success - close modal and refresh the file tree + hideModal('addDirModal'); + + // Reload workspace directories to show the newly added directory + await loadWorkspaceDirectories(); + + // Show success message + showStatus('Directory added successfully!', 'success'); + }); + } + // Chat functionality initChatInterface(); }); diff --git a/lib/web/ui/templates/heroprompt.html b/lib/web/ui/templates/heroprompt.html index e0c5299f..757dde76 100644 --- a/lib/web/ui/templates/heroprompt.html +++ b/lib/web/ui/templates/heroprompt.html @@ -86,6 +86,10 @@
+
@@ -310,12 +314,10 @@ Example: @@ -340,10 +342,6 @@ Example: -
- - -
+ + +