PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/workspace.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:00:26 UTC 6,926 bytes, 160 lines download raw bytes

1// static/js/workspace.js
2//
3// Workspace picker: browse server directories in a draggable modal, choose a
4// folder, and show it as a removable pill in the chat input bar. While set, the
5// chat request sends `workspace` so the agent's file/shell tools are confined
6// to that folder (see routes/chat_routes.py + src/tool_execution.py).
7
8import Storage, { KEYS } from './storage.js';
9import uiModule from './ui.js';
10import { makeWindowDraggable } from './windowDrag.js';
11
12const API_BASE = window.location.origin;
13// Same folder glyph as the overflow menu item + pill (not an emoji).
14const _FOLDER_SVG = '<svg class="workspace-row-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>';
15let _modal = null;
16let _curPath = '';
17
18export function getWorkspace() {
19  return Storage.get(KEYS.WORKSPACE, '') || '';
20}
21
22function _basename(p) {
23  if (!p) return '';
24  // Handle both POSIX (/) and Windows (\) separators.
25  const parts = p.replace(/[\\/]+$/, '').split(/[\\/]/);
26  return parts[parts.length - 1] || p;
27}
28
29export function syncWorkspaceIndicator(path) {
30  const pill = document.getElementById('workspace-indicator-btn');
31  const name = document.getElementById('workspace-indicator-name');
32  const overflow = document.getElementById('overflow-workspace-btn');
33  if (pill) {
34    pill.style.display = path ? '' : 'none';
35    pill.classList.toggle('active', !!path);
36    if (path) pill.title = `Workspace: ${path} — click to clear`;
37  }
38  if (name) name.textContent = path ? _basename(path) : '';
39  if (overflow) overflow.classList.toggle('active', !!path);
40  // Recompute the "+" overflow dot (app.js owns updatePlusDot via this event).
41  try { document.dispatchEvent(new CustomEvent('overflow-state-change')); } catch (_) {}
42}
43
44export function setWorkspace(path) {
45  if (path) Storage.set(KEYS.WORKSPACE, path);
46  else Storage.remove(KEYS.WORKSPACE);
47  syncWorkspaceIndicator(path || '');
48}
49
50export function clearWorkspace() {
51  setWorkspace('');
52  if (uiModule && uiModule.showToast) uiModule.showToast('Workspace cleared');
53}
54
55async function _load(path) {
56  const url = `${API_BASE}/api/workspace/browse${path ? `?path=${encodeURIComponent(path)}` : ''}`;
57  const res = await fetch(url, { credentials: 'same-origin' });
58  if (!res.ok) throw new Error(`browse failed: ${res.status}`);
59  return res.json();
60}
61
62function _render(data) {
63  _curPath = data.path;
64  const body = _modal.querySelector('#workspace-body');
65  const pathEl = _modal.querySelector('#workspace-cur-path');
66  if (pathEl) {
67    // Reflect the resolved (realpath) location back into the editable field.
68    pathEl.value = data.path;
69    pathEl.title = data.path;
70  }
71  let rows = '';
72  if (data.parent) {
73    rows += `<div class="workspace-row workspace-up" data-path="${encodeURIComponent(data.parent)}">↑ ..</div>`;
74  }
75  for (const d of data.dirs) {
76    // Backend supplies the full child path (os.path.join → cross-platform).
77    rows += `<div class="workspace-row" data-path="${encodeURIComponent(d.path)}">${_FOLDER_SVG}<span>${uiModule.esc(d.name)}</span></div>`;
78  }
79  if (!data.dirs.length && !data.parent) rows = '<div class="workspace-empty">No subfolders</div>';
80  body.innerHTML = rows || '<div class="workspace-empty">No subfolders</div>';
81  body.querySelectorAll('.workspace-row').forEach((row) => {
82    row.addEventListener('click', () => _navigate(decodeURIComponent(row.dataset.path)));
83  });
84}
85
86async function _navigate(path) {
87  try {
88    _render(await _load(path));
89  } catch (e) {
90    if (uiModule && uiModule.showError) uiModule.showError('Could not open folder');
91  }
92}
93
94function _getModal() {
95  if (_modal) return _modal;
96  _modal = document.createElement('div');
97  _modal.id = 'workspace-modal';
98  _modal.className = 'modal';
99  _modal.style.display = 'none';
100  _modal.innerHTML = `
101    <div class="modal-content">
102      <div class="modal-header">
103        <h4><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>Select workspace</h4>
104        <button class="close-btn" id="workspace-close" aria-label="Close">✖</button>
105      </div>
106      <input type="text" class="styled-prompt-input workspace-cur" id="workspace-cur-path"
107             spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off"
108             placeholder="Type or paste a folder path, then press Enter" />
109      <div class="modal-body workspace-body" id="workspace-body"></div>
110      <div class="modal-footer workspace-footer">
111        <button type="button" class="confirm-btn confirm-btn-secondary" id="workspace-cancel">Cancel</button>
112        <button type="button" class="confirm-btn confirm-btn-primary" id="workspace-use">Use this folder</button>
113      </div>
114    </div>`;
115  document.body.appendChild(_modal);
116  _modal.querySelector('#workspace-close').addEventListener('click', closeWorkspaceBrowser);
117  _modal.querySelector('#workspace-cancel').addEventListener('click', closeWorkspaceBrowser);
118  // Editable path bar: Enter navigates to a typed/pasted folder.
119  _modal.querySelector('#workspace-cur-path').addEventListener('keydown', (e) => {
120    if (e.key === 'Enter') {
121      e.preventDefault();
122      const v = e.target.value.trim();
123      if (v) _navigate(v);
124    }
125  });
126  _modal.querySelector('#workspace-use').addEventListener('click', () => {
127    setWorkspace(_curPath);
128    if (uiModule && uiModule.showToast) uiModule.showToast(`Workspace set: ${_basename(_curPath)}`);
129    closeWorkspaceBrowser();
130  });
131  const content = _modal.querySelector('.modal-content');
132  const header = _modal.querySelector('.modal-header');
133  if (content && header) makeWindowDraggable(_modal, { content, header });
134  return _modal;
135}
136
137export async function openWorkspaceBrowser() {
138  const modal = _getModal();
139  modal.style.display = 'flex';
140  try {
141    _render(await _load(getWorkspace() || ''));
142  } catch (e) {
143    if (uiModule && uiModule.showError) uiModule.showError('Could not browse folders');
144  }
145}
146
147export function closeWorkspaceBrowser() {
148  if (_modal) _modal.style.display = 'none';
149}
150
151export function initWorkspace() {
152  // Restore persisted workspace into the pill on load.
153  syncWorkspaceIndicator(getWorkspace());
154  const overflow = document.getElementById('overflow-workspace-btn');
155  if (overflow) overflow.addEventListener('click', openWorkspaceBrowser);
156  const pill = document.getElementById('workspace-indicator-btn');
157  if (pill) pill.addEventListener('click', clearWorkspace);
158}
159
160export default { initWorkspace, openWorkspaceBrowser, getWorkspace, setWorkspace, clearWorkspace, syncWorkspaceIndicator };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.