lazyclaw 3.99.28 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -324,6 +324,9 @@
324
324
  <button data-tab="doctor">Doctor</button>
325
325
  <button data-tab="config">Config</button>
326
326
  <button data-tab="status">Status</button>
327
+ <button data-tab="agents">Agents</button>
328
+ <button data-tab="teams">Teams</button>
329
+ <button data-tab="tasks">Tasks</button>
327
330
  </nav>
328
331
 
329
332
  <main>
@@ -429,6 +432,35 @@
429
432
  <pre id="config-raw"></pre>
430
433
  </details>
431
434
  </section>
435
+
436
+ <section id="tab-agents">
437
+ <h2>Agents</h2>
438
+ <div class="toolbar">
439
+ <button class="btn" onclick="openAgentModal()">+ New agent</button>
440
+ <button class="btn btn-secondary" onclick="LOADERS.agents()">Refresh</button>
441
+ <span class="dim" id="agents-meta"></span>
442
+ </div>
443
+ <div id="agents-list"><div class="empty">Loading…</div></div>
444
+ </section>
445
+
446
+ <section id="tab-teams">
447
+ <h2>Teams</h2>
448
+ <div class="toolbar">
449
+ <button class="btn" onclick="openTeamModal()">+ New team</button>
450
+ <button class="btn btn-secondary" onclick="LOADERS.teams()">Refresh</button>
451
+ <span class="dim" id="teams-meta"></span>
452
+ </div>
453
+ <div id="teams-list"><div class="empty">Loading…</div></div>
454
+ </section>
455
+
456
+ <section id="tab-tasks">
457
+ <h2>Tasks</h2>
458
+ <div class="toolbar">
459
+ <button class="btn btn-secondary" onclick="LOADERS.tasks()">Refresh</button>
460
+ <span class="dim" id="tasks-meta">Tasks are created via <code>lazyclaw task start</code>.</span>
461
+ </div>
462
+ <div id="tasks-list"><div class="empty">Loading…</div></div>
463
+ </section>
432
464
  </main>
433
465
 
434
466
  <footer>
@@ -1351,6 +1383,140 @@
1351
1383
  }
1352
1384
  }
1353
1385
 
1386
+ // ── Multi-agent loaders (Phase 15) ────────────────────────────
1387
+ // Keep these minimal — list view + prompt-driven create. Phase
1388
+ // 15.1+ can swap the prompts for inline forms once the data model
1389
+ // settles. The point of v0.1 is parity with the CLI, not polish.
1390
+
1391
+ function escapeHtml(s) {
1392
+ return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
1393
+ }
1394
+
1395
+ LOADERS.agents = async function loadAgents() {
1396
+ const root = document.getElementById('agents-list');
1397
+ try {
1398
+ const arr = await api('/agents');
1399
+ document.getElementById('agents-meta').textContent = `${arr.length} agent(s)`;
1400
+ if (arr.length === 0) { root.innerHTML = '<div class="empty">No agents yet — click + New agent to create one.</div>'; return; }
1401
+ root.innerHTML = '<table><thead><tr><th>name</th><th>provider/model</th><th>tools</th><th>role (excerpt)</th><th></th></tr></thead><tbody>'
1402
+ + arr.map((a) => {
1403
+ const provLine = a.model ? `${escapeHtml(a.provider)}/${escapeHtml(a.model)}` : escapeHtml(a.provider);
1404
+ const role = a.role ? (a.role.slice(0, 60) + (a.role.length > 60 ? '…' : '')) : '<span class="dim">(none)</span>';
1405
+ return `<tr>
1406
+ <td><strong>${escapeHtml(a.name)}</strong><br><span class="dim">${escapeHtml(a.displayName || '')}</span></td>
1407
+ <td>${provLine}</td>
1408
+ <td>${(a.tools || []).map((t) => `<code>${escapeHtml(t)}</code>`).join(' ')}</td>
1409
+ <td>${escapeHtml(role)}</td>
1410
+ <td><button class="btn btn-secondary" onclick="deleteAgent('${encodeURIComponent(a.name)}')">Delete</button></td>
1411
+ </tr>`;
1412
+ }).join('')
1413
+ + '</tbody></table>';
1414
+ } catch (e) {
1415
+ root.innerHTML = `<div class="empty">Error: ${escapeHtml(e.message)}</div>`;
1416
+ }
1417
+ };
1418
+
1419
+ async function openAgentModal() {
1420
+ const name = (prompt('Agent name (e.g. planner, backend, frontend):') || '').trim();
1421
+ if (!name) return;
1422
+ const role = prompt('Role / system prompt (optional):') || '';
1423
+ const provider = (prompt('Provider (anthropic / openai / gemini / claude-cli):', 'anthropic') || 'anthropic').trim();
1424
+ const model = (prompt('Model id (blank = provider default):') || '').trim();
1425
+ const toolsRaw = (prompt('Tools (comma-separated):', 'bash,read,write,grep') || '').trim();
1426
+ const tools = toolsRaw ? toolsRaw.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
1427
+ try {
1428
+ await api('/agents', { method: 'POST', body: JSON.stringify({ name, role, provider, model, tools }) });
1429
+ LOADERS.agents();
1430
+ } catch (e) {
1431
+ alert('Create failed: ' + e.message);
1432
+ }
1433
+ }
1434
+
1435
+ async function deleteAgent(encName) {
1436
+ const name = decodeURIComponent(encName);
1437
+ if (!confirm(`Delete agent "${name}"?`)) return;
1438
+ try { await api(`/agents/${encName}`, { method: 'DELETE' }); LOADERS.agents(); }
1439
+ catch (e) { alert('Delete failed: ' + e.message); }
1440
+ }
1441
+
1442
+ LOADERS.teams = async function loadTeams() {
1443
+ const root = document.getElementById('teams-list');
1444
+ try {
1445
+ const arr = await api('/teams');
1446
+ document.getElementById('teams-meta').textContent = `${arr.length} team(s)`;
1447
+ if (arr.length === 0) { root.innerHTML = '<div class="empty">No teams yet — click + New team to create one.</div>'; return; }
1448
+ root.innerHTML = '<table><thead><tr><th>name</th><th>lead</th><th>agents</th><th>slack channel</th><th></th></tr></thead><tbody>'
1449
+ + arr.map((t) => `<tr>
1450
+ <td><strong>${escapeHtml(t.name)}</strong><br><span class="dim">${escapeHtml(t.displayName || '')}</span></td>
1451
+ <td>${escapeHtml(t.lead || '')}</td>
1452
+ <td>${(t.agents || []).map((a) => escapeHtml(a)).join(', ')}</td>
1453
+ <td>${t.slackChannel ? `<code>${escapeHtml(t.slackChannel)}</code>` : '<span class="dim">(none)</span>'}</td>
1454
+ <td><button class="btn btn-secondary" onclick="deleteTeam('${encodeURIComponent(t.name)}')">Delete</button></td>
1455
+ </tr>`).join('')
1456
+ + '</tbody></table>';
1457
+ } catch (e) {
1458
+ root.innerHTML = `<div class="empty">Error: ${escapeHtml(e.message)}</div>`;
1459
+ }
1460
+ };
1461
+
1462
+ async function openTeamModal() {
1463
+ const name = (prompt('Team name (e.g. shop, growth):') || '').trim();
1464
+ if (!name) return;
1465
+ const agentsRaw = (prompt('Agents (comma-separated names):') || '').trim();
1466
+ if (!agentsRaw) return;
1467
+ const agents = agentsRaw.split(',').map((s) => s.trim()).filter(Boolean);
1468
+ const lead = (prompt(`Lead (one of ${agents.join(', ')}):`, agents[0]) || agents[0]).trim();
1469
+ const slackChannel = (prompt('Slack channel (C… id or #name, optional):') || '').trim();
1470
+ try {
1471
+ await api('/teams', { method: 'POST', body: JSON.stringify({ name, agents, lead, slackChannel }) });
1472
+ LOADERS.teams();
1473
+ } catch (e) {
1474
+ alert('Create failed: ' + e.message);
1475
+ }
1476
+ }
1477
+
1478
+ async function deleteTeam(encName) {
1479
+ const name = decodeURIComponent(encName);
1480
+ if (!confirm(`Delete team "${name}"?`)) return;
1481
+ try { await api(`/teams/${encName}`, { method: 'DELETE' }); LOADERS.teams(); }
1482
+ catch (e) { alert('Delete failed: ' + e.message); }
1483
+ }
1484
+
1485
+ LOADERS.tasks = async function loadTasks() {
1486
+ const root = document.getElementById('tasks-list');
1487
+ try {
1488
+ const arr = await api('/tasks');
1489
+ document.getElementById('tasks-meta').textContent = `${arr.length} task(s) (newest first)`;
1490
+ if (arr.length === 0) { root.innerHTML = '<div class="empty">No tasks yet. Run <code>lazyclaw task start --team X --title "..."</code>.</div>'; return; }
1491
+ root.innerHTML = '<table><thead><tr><th>id</th><th>title</th><th>team</th><th>lead</th><th>status</th><th>turns</th><th>opened</th><th></th></tr></thead><tbody>'
1492
+ + arr.map((t) => `<tr>
1493
+ <td><code>${escapeHtml(t.id)}</code></td>
1494
+ <td>${escapeHtml(t.title)}</td>
1495
+ <td>${escapeHtml(t.team)}</td>
1496
+ <td>${escapeHtml(t.lead)}</td>
1497
+ <td><span class="status status-${escapeHtml(t.status)}">${escapeHtml(t.status)}</span></td>
1498
+ <td>${Array.isArray(t.turns) ? t.turns.length : 0}</td>
1499
+ <td><span class="dim">${escapeHtml((t.createdAt || '').slice(0, 19))}</span></td>
1500
+ <td>
1501
+ ${t.status === 'running' || t.status === 'pending'
1502
+ ? `<button class="btn btn-secondary" onclick="closeTask('${encodeURIComponent(t.id)}','done')">Mark done</button>
1503
+ <button class="btn btn-secondary" onclick="closeTask('${encodeURIComponent(t.id)}','abandon')">Abandon</button>`
1504
+ : ''}
1505
+ </td>
1506
+ </tr>`).join('')
1507
+ + '</tbody></table>';
1508
+ } catch (e) {
1509
+ root.innerHTML = `<div class="empty">Error: ${escapeHtml(e.message)}</div>`;
1510
+ }
1511
+ };
1512
+
1513
+ async function closeTask(encId, action) {
1514
+ const id = decodeURIComponent(encId);
1515
+ if (!confirm(`${action === 'done' ? 'Mark done' : 'Abandon'} task ${id}?`)) return;
1516
+ try { await api(`/tasks/${encId}/${action}`, { method: 'POST' }); LOADERS.tasks(); }
1517
+ catch (e) { alert(`${action} failed: ` + e.message); }
1518
+ }
1519
+
1354
1520
  // First load = chat tab.
1355
1521
  LOADERS.chat();
1356
1522