feat: Support multi-root workspaces

- Remove `base_path` from Workspace struct and APIs
- Enable adding multiple root directories to a workspace
- Update file tree UI to display all workspace roots
- Refactor file map generation for multi-root display
- Improve prompt output clipboard copy with status
This commit is contained in:
Mahmoud-Emad
2025-09-07 14:40:17 +03:00
parent 145c6d8714
commit 63c0b81fc9
7 changed files with 483 additions and 166 deletions

View File

@@ -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)!

View File

@@ -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{}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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 = `
<div class="file-card-header">
<div class="file-card-icon">
@@ -402,7 +405,7 @@ class SimpleFileTree {
</div>
<div class="file-card-info">
<h4 class="file-card-name">${fileName}</h4>
<p class="file-card-path">${path}</p>
<p class="file-card-path">${displayPath}</p>
</div>
</div>
<div class="file-card-metadata">
@@ -825,6 +828,66 @@ class SimpleFileTree {
this.updateSelectionUI();
}
async renderWorkspaceDirectories(directories) {
this.container.innerHTML = '<div class="loading">Loading workspace directories...</div>';
// Reset state
this.loadedPaths.clear();
this.expandedDirs.clear();
expandedDirs.clear();
if (!directories || directories.length === 0) {
this.container.innerHTML = `
<div class="empty-state">
<i class="icon-folder-open"></i>
<p>No directories added yet</p>
<small>Use the "Add Dir" button to add directories to this workspace</small>
</div>
`;
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 = `
<div class="empty-state">
<i class="icon-folder-open"></i>
<p>No directories added yet</p>
<small>Use the "Add Dir" button to add directories to this workspace</small>
</div>
`;
return;
}
// Filter only directories
const directories = children.filter(child => child.path && child.path.cat === 'dir');
if (directories.length === 0) {
treeEl.innerHTML = `
<div class="empty-state">
<i class="icon-folder-open"></i>
<p>No directories added yet</p>
<small>Use the "Add Dir" button to add directories to this workspace</small>
</div>
`;
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 = `
<div class="empty-state">
<i class="icon-folder-open"></i>
<p>Error loading directories</p>
<small>Please try refreshing the page</small>
</div>
`;
}
}
@@ -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 = '<div class="success-message">Prompt copied to clipboard!</div>';
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 = '<div class="error-message">Failed to copy prompt</div>';
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 = `<div class="${statusClass}">${msg}</div>`;
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 = `
<div class="empty-state">
<i class="icon-folder-open"></i>
<p>No directories added yet</p>
<small>Use the "Add Dir" button to add directories to this workspace</small>
</div>
`;
}
}
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();
});

View File

@@ -86,6 +86,10 @@
</span>
</div>
<div class="selection-actions">
<button id="addDirBtn" class="btn btn-sm btn-primary" title="Add Directory">
<i class="icon-plus"></i>
Add Dir
</button>
</div>
</div>
</div>
@@ -310,12 +314,10 @@ Example:
</div>
<div class="modal-body">
<div class="mb-3">
<label for="wcName" class="form-label">Workspace Name (optional)</label>
<input type="text" class="form-control" id="wcName" placeholder="Enter workspace name">
</div>
<div class="mb-3">
<label for="wcPath" class="form-label">Base Path (required)</label>
<input type="text" class="form-control" id="wcPath" placeholder="Enter base directory path">
<label for="wcName" class="form-label">Workspace Name (required)</label>
<input type="text" class="form-control" id="wcName" placeholder="Enter workspace name" required>
<div class="form-text">Choose a unique name for your workspace. You can add directories to it
after creation.</div>
</div>
<div id="wcError" class="text-danger small"></div>
</div>
@@ -340,10 +342,6 @@ Example:
<label for="wdName" class="form-label">Workspace Name</label>
<input type="text" class="form-control" id="wdName">
</div>
<div class="mb-3">
<label for="wdPath" class="form-label">Base Path</label>
<input type="text" class="form-control" id="wdPath">
</div>
<div id="wdError" class="text-danger small"></div>
</div>
<div class="modal-footer">
@@ -392,6 +390,32 @@ Example:
</div>
</div>
<!-- Add Directory Modal -->
<div class="modal fade" id="addDirModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Directory to Workspace</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="addDirPath" class="form-label">Directory Path</label>
<input type="text" class="form-control" id="addDirPath"
placeholder="Enter directory path (e.g., /path/to/directory)">
<div class="form-text">Enter the full path to the directory you want to add to the workspace.
</div>
</div>
<div id="addDirError" class="text-danger small"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="addDirConfirm">Add Directory</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>