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:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user