// static/js/workspace.js // // Workspace picker: browse server directories in a draggable modal, choose a // folder, and show it as a removable pill in the chat input bar. While set, the // chat request sends `workspace` so the agent's file/shell tools are confined // to that folder (see routes/chat_routes.py + src/tool_execution.py). import Storage, { KEYS } from './storage.js'; import uiModule from './ui.js'; import { makeWindowDraggable } from './windowDrag.js'; const API_BASE = window.location.origin; // Same folder glyph as the overflow menu item + pill (not an emoji). const _FOLDER_SVG = ''; let _modal = null; let _curPath = ''; export function getWorkspace() { return Storage.get(KEYS.WORKSPACE, '') || ''; } function _basename(p) { if (!p) return ''; // Handle both POSIX (/) and Windows (\) separators. const parts = p.replace(/[\\/]+$/, '').split(/[\\/]/); return parts[parts.length - 1] || p; } export function syncWorkspaceIndicator(path) { const pill = document.getElementById('workspace-indicator-btn'); const name = document.getElementById('workspace-indicator-name'); const overflow = document.getElementById('overflow-workspace-btn'); if (pill) { pill.style.display = path ? '' : 'none'; pill.classList.toggle('active', !!path); if (path) pill.title = `Workspace: ${path} — click to clear`; } if (name) name.textContent = path ? _basename(path) : ''; if (overflow) overflow.classList.toggle('active', !!path); // Recompute the "+" overflow dot (app.js owns updatePlusDot via this event). try { document.dispatchEvent(new CustomEvent('overflow-state-change')); } catch (_) {} } export function setWorkspace(path) { if (path) Storage.set(KEYS.WORKSPACE, path); else Storage.remove(KEYS.WORKSPACE); syncWorkspaceIndicator(path || ''); } export function clearWorkspace() { setWorkspace(''); if (uiModule && uiModule.showToast) uiModule.showToast('Workspace cleared'); } async function _load(path) { const url = `${API_BASE}/api/workspace/browse${path ? `?path=${encodeURIComponent(path)}` : ''}`; const res = await fetch(url, { credentials: 'same-origin' }); if (!res.ok) throw new Error(`browse failed: ${res.status}`); return res.json(); } function _render(data) { _curPath = data.path; const body = _modal.querySelector('#workspace-body'); const pathEl = _modal.querySelector('#workspace-cur-path'); if (pathEl) { // Reflect the resolved (realpath) location back into the editable field. pathEl.value = data.path; pathEl.title = data.path; } let rows = ''; if (data.parent) { rows += `
↑ ..
`; } for (const d of data.dirs) { // Backend supplies the full child path (os.path.join → cross-platform). rows += `
${_FOLDER_SVG}${uiModule.esc(d.name)}
`; } if (!data.dirs.length && !data.parent) rows = '
No subfolders
'; body.innerHTML = rows || '
No subfolders
'; body.querySelectorAll('.workspace-row').forEach((row) => { row.addEventListener('click', () => _navigate(decodeURIComponent(row.dataset.path))); }); } async function _navigate(path) { try { _render(await _load(path)); } catch (e) { if (uiModule && uiModule.showError) uiModule.showError('Could not open folder'); } } function _getModal() { if (_modal) return _modal; _modal = document.createElement('div'); _modal.id = 'workspace-modal'; _modal.className = 'modal'; _modal.style.display = 'none'; _modal.innerHTML = ` `; document.body.appendChild(_modal); _modal.querySelector('#workspace-close').addEventListener('click', closeWorkspaceBrowser); _modal.querySelector('#workspace-cancel').addEventListener('click', closeWorkspaceBrowser); // Editable path bar: Enter navigates to a typed/pasted folder. _modal.querySelector('#workspace-cur-path').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const v = e.target.value.trim(); if (v) _navigate(v); } }); _modal.querySelector('#workspace-use').addEventListener('click', () => { setWorkspace(_curPath); if (uiModule && uiModule.showToast) uiModule.showToast(`Workspace set: ${_basename(_curPath)}`); closeWorkspaceBrowser(); }); const content = _modal.querySelector('.modal-content'); const header = _modal.querySelector('.modal-header'); if (content && header) makeWindowDraggable(_modal, { content, header }); return _modal; } export async function openWorkspaceBrowser() { const modal = _getModal(); modal.style.display = 'flex'; try { _render(await _load(getWorkspace() || '')); } catch (e) { if (uiModule && uiModule.showError) uiModule.showError('Could not browse folders'); } } export function closeWorkspaceBrowser() { if (_modal) _modal.style.display = 'none'; } export function initWorkspace() { // Restore persisted workspace into the pill on load. syncWorkspaceIndicator(getWorkspace()); const overflow = document.getElementById('overflow-workspace-btn'); if (overflow) overflow.addEventListener('click', openWorkspaceBrowser); const pill = document.getElementById('workspace-indicator-btn'); if (pill) pill.addEventListener('click', clearWorkspace); } export default { initWorkspace, openWorkspaceBrowser, getWorkspace, setWorkspace, clearWorkspace, syncWorkspaceIndicator };