diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-07 00:06:44 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-07 00:06:44 +0000 |
| commit | 0291a7880d29b39d7cd56a6a8be66a9b5ec3f457 (patch) | |
| tree | 2ed6fda247c12f590cc05e4e5150c165c42a5640 | |
| parent | 5f1bfde96d6a36381987c0f700d66e1099d5f8f9 (diff) | |
ui: Project dropdown in new task dialog, first field, defaults to /workspace/claudomator
- Moved working directory to first field, renamed to "Project"
- Replaced text input with a select populated from GET /api/workspaces
(lists subdirs of /workspace dynamically)
- "Create new project…" option reveals a custom path input
- elaborate result handler sets select or falls back to new-project input
- Added GET /api/workspaces endpoint in server.go
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/api/server.go | 16 | ||||
| -rw-r--r-- | web/app.js | 64 | ||||
| -rw-r--r-- | web/index.html | 9 |
3 files changed, 84 insertions, 5 deletions
diff --git a/internal/api/server.go b/internal/api/server.go index dd4627c..af4710b 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -87,6 +87,7 @@ func (s *Server) routes() { s.mux.HandleFunc("POST /api/scripts/start-next-task", s.handleStartNextTask) s.mux.HandleFunc("POST /api/scripts/deploy", s.handleDeploy) s.mux.HandleFunc("GET /api/ws", s.handleWebSocket) + s.mux.HandleFunc("GET /api/workspaces", s.handleListWorkspaces) s.mux.HandleFunc("GET /api/health", s.handleHealth) s.mux.Handle("GET /", http.FileServerFS(webui.Files)) } @@ -252,6 +253,21 @@ func (s *Server) handleResumeTimedOutTask(w http.ResponseWriter, r *http.Request }) } +func (s *Server) handleListWorkspaces(w http.ResponseWriter, r *http.Request) { + entries, err := os.ReadDir("/workspace") + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + var dirs []string + for _, e := range entries { + if e.IsDir() { + dirs = append(dirs, "/workspace/"+e.Name()) + } + } + writeJSON(w, http.StatusOK, dirs) +} + func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } @@ -1046,8 +1046,50 @@ function renderValidationResult(result) { // ── Task modal ──────────────────────────────────────────────────────────────── -function openTaskModal() { +async function openTaskModal() { document.getElementById('task-modal').showModal(); + await populateProjectSelect(); +} + +async function populateProjectSelect() { + const select = document.getElementById('project-select'); + const current = select.value; + try { + const res = await fetch(`${API_BASE}/api/workspaces`); + const dirs = await res.json(); + select.innerHTML = ''; + dirs.forEach(dir => { + const opt = document.createElement('option'); + opt.value = dir; + opt.textContent = dir; + if (dir === current || dir === '/workspace/claudomator') opt.selected = true; + select.appendChild(opt); + }); + } catch { + // keep whatever options are already there + } + // Ensure "Create new project…" option is always last + const newOpt = document.createElement('option'); + newOpt.value = '__new__'; + newOpt.textContent = 'Create new project…'; + select.appendChild(newOpt); +} + +function initProjectSelect() { + const select = document.getElementById('project-select'); + const newRow = document.getElementById('new-project-row'); + const newInput = document.getElementById('new-project-input'); + select.addEventListener('change', () => { + if (select.value === '__new__') { + newRow.hidden = false; + newInput.required = true; + newInput.focus(); + } else { + newRow.hidden = true; + newInput.required = false; + newInput.value = ''; + } + }); } function closeTaskModal() { @@ -1061,13 +1103,17 @@ function closeTaskModal() { } async function createTask(formData) { + const selectVal = formData.get('working_dir'); + const workingDir = selectVal === '__new__' + ? document.getElementById('new-project-input').value.trim() + : selectVal; const body = { name: formData.get('name'), description: '', claude: { model: formData.get('model'), instructions: formData.get('instructions'), - working_dir: formData.get('working_dir'), + working_dir: workingDir, max_budget_usd: parseFloat(formData.get('max_budget_usd')), }, timeout: formData.get('timeout'), @@ -1608,6 +1654,7 @@ if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded // Task modal document.getElementById('btn-new-task').addEventListener('click', openTaskModal); document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal); + initProjectSelect(); // Validate button document.getElementById('btn-validate').addEventListener('click', async () => { @@ -1660,8 +1707,17 @@ if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded f.querySelector('[name="name"]').value = result.name; if (result.claude && result.claude.instructions) f.querySelector('[name="instructions"]').value = result.claude.instructions; - if (result.claude && result.claude.working_dir) - f.querySelector('[name="working_dir"]').value = result.claude.working_dir; + if (result.claude && result.claude.working_dir) { + const sel = document.getElementById('project-select'); + const exists = [...sel.options].some(o => o.value === result.claude.working_dir); + if (exists) { + sel.value = result.claude.working_dir; + } else { + sel.value = '__new__'; + document.getElementById('new-project-row').hidden = false; + document.getElementById('new-project-input').value = result.claude.working_dir; + } + } if (result.claude && result.claude.model) f.querySelector('[name="model"]').value = result.claude.model; if (result.claude && result.claude.max_budget_usd != null) diff --git a/web/index.html b/web/index.html index 7afeb76..8bfa6bb 100644 --- a/web/index.html +++ b/web/index.html @@ -56,6 +56,14 @@ <p class="elaborate-hint">Claude will fill in the form fields below. You can edit before submitting.</p> </div> <hr class="form-divider"> + <label>Project + <select name="working_dir" id="project-select"> + <option value="/workspace/claudomator" selected>/workspace/claudomator</option> + </select> + </label> + <div id="new-project-row" hidden> + <label>New project path <input id="new-project-input" placeholder="/workspace/my-project"></label> + </div> <label>Name <input name="name" required></label> <label>Instructions <textarea name="instructions" rows="6" required></textarea></label> <div class="validate-section"> @@ -64,7 +72,6 @@ </button> <div id="validate-result" hidden></div> </div> - <label>Working Directory <input name="working_dir" placeholder="/path/to/repo"></label> <label>Model <input name="model" value="sonnet"></label> <label>Max Budget (USD) <input name="max_budget_usd" type="number" step="0.01" value="1.00"></label> <label>Timeout <input name="timeout" value="15m"></label> |
