diff --git a/src/web/lisa-client.ts b/src/web/lisa-client.ts index 245941e..5099b18 100644 --- a/src/web/lisa-client.ts +++ b/src/web/lisa-client.ts @@ -1039,6 +1039,17 @@ if ('serviceWorker' in navigator) { return bits.join(' · '); } + // POST a managed-agent control action (start/send/cancel/approve), then refresh. + function managedAction(id, action, body) { + fetch('/api/agents/managed/' + encodeURIComponent(id) + '/' + action, { + method: 'POST', + headers: body ? { 'content-type': 'application/json' } : {}, + body: body ? JSON.stringify(body) : undefined, + }).then(function () { + if (typeof refreshClaudeSessions === 'function') refreshClaudeSessions(); + }).catch(function () {}); + } + function setClaudeSessions(sessions) { const cutoff = Date.now() - ACTIVE_WINDOW_MS; const recent = sessions.filter(s => new Date(s.lastMtime).getTime() >= cutoff); @@ -1100,6 +1111,38 @@ if ('serviceWorker' in navigator) { act.title = actText; row.appendChild(act); } + // Managed agents are controllable: approve/deny a pending tool, send a + // follow-up, or cancel. (Externally-started CLIs aren't — observe only.) + if (s.agent === 'managed') { + const id = s.sessionId; + const ctrl = document.createElement('div'); + ctrl.className = 'session-ctrl'; + const pending = s.activity && s.activity.pendingPermission; + if (pending) { + const ap = document.createElement('button'); + ap.className = 'mc approve'; ap.textContent = '✓ approve'; + ap.addEventListener('click', function (e) { e.stopPropagation(); managedAction(id, 'approve', { allow: true }); }); + const dn = document.createElement('button'); + dn.className = 'mc deny'; dn.textContent = '✕ deny'; + dn.addEventListener('click', function (e) { e.stopPropagation(); managedAction(id, 'approve', { allow: false }); }); + ctrl.appendChild(ap); ctrl.appendChild(dn); + } else if (s.state !== 'done') { + const inp = document.createElement('input'); + inp.className = 'mc-send'; inp.type = 'text'; inp.placeholder = 'send a follow-up…'; + inp.addEventListener('click', function (e) { e.stopPropagation(); }); + inp.addEventListener('keydown', function (e) { + if (e.key === 'Enter' && inp.value.trim()) { e.preventDefault(); managedAction(id, 'send', { text: inp.value.trim() }); inp.value = ''; } + }); + ctrl.appendChild(inp); + } + if (s.state !== 'done') { + const cancel = document.createElement('button'); + cancel.className = 'mc cancel'; cancel.textContent = '⏹'; cancel.title = 'Cancel agent'; + cancel.addEventListener('click', function (e) { e.stopPropagation(); managedAction(id, 'cancel', null); }); + ctrl.appendChild(cancel); + } + if (ctrl.childNodes.length) row.appendChild(ctrl); + } row.title = (s.stateReason ? s.state + ' · ' + s.stateReason : s.state) + ' · ' + s.project + ' · ' + s.sessionId; sbClaudeRows.appendChild(row); } @@ -1129,6 +1172,25 @@ if ('serviceWorker' in navigator) { } catch {} }; + // "Delegate a task" → start a managed agent (LISA-run, controllable). + const sbDelegate = document.getElementById('sbDelegate'); + if (sbDelegate) { + sbDelegate.addEventListener('submit', function (e) { + e.preventDefault(); + const inp = document.getElementById('sbDelegateTask'); + const task = inp && inp.value.trim(); + if (!task) return; + fetch('/api/agents/managed/start', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ task: task }), + }).then(function () { + if (inp) inp.value = ''; + if (typeof refreshClaudeSessions === 'function') refreshClaudeSessions(); + }).catch(function () {}); + }); + } + async function refreshIdentity() { try { const r = await fetch('/api/soul'); diff --git a/src/web/lisa-css.ts b/src/web/lisa-css.ts index f29880e..98f7d01 100644 --- a/src/web/lisa-css.ts +++ b/src/web/lisa-css.ts @@ -333,6 +333,63 @@ export const MAIN_CSS = ` :root { text-overflow: ellipsis; white-space: nowrap; } + /* Managed-agent controls (approve/deny · send follow-up · cancel). */ + .session-row .session-ctrl { + grid-column: 2 / -1; + margin-top: 4px; + display: flex; + gap: 6px; + align-items: center; + flex-wrap: wrap; + } + .session-ctrl .mc { + font-size: 10px; + padding: 2px 8px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--panel2, rgba(255,255,255,0.05)); + color: var(--fg); + cursor: pointer; + } + .session-ctrl .mc.approve { color: var(--green, #6bff9d); border-color: rgba(107,255,157,0.4); } + .session-ctrl .mc.deny, + .session-ctrl .mc.cancel { color: var(--err-color, #ff5577); border-color: rgba(255,85,119,0.4); } + .session-ctrl .mc:hover { background: rgba(255,255,255,0.10); } + .session-ctrl .mc-send { + flex: 1; + min-width: 90px; + font-size: 10.5px; + padding: 2px 7px; + border-radius: 6px; + border: 1px solid var(--border); + background: rgba(0,0,0,0.25); + color: var(--fg); + } + /* "Delegate a task" composer at the top of the agents card. */ + .delegate { + display: flex; + gap: 6px; + margin: 2px 0 8px; + } + .delegate input { + flex: 1; + font-size: 11px; + padding: 4px 8px; + border-radius: 7px; + border: 1px solid var(--border); + background: rgba(0,0,0,0.25); + color: var(--fg); + } + .delegate button { + font-size: 11px; + padding: 4px 10px; + border-radius: 7px; + border: 1px solid var(--claude, #ff8c42); + background: rgba(255,140,66,0.16); + color: var(--claude, #ff8c42); + cursor: pointer; + } + .delegate button:hover { background: rgba(255,140,66,0.28); } .session-empty { color: var(--fg-faint); font-size: 11.5px; diff --git a/src/web/lisa-html-snapshot.test.ts b/src/web/lisa-html-snapshot.test.ts index 28bedae..45b1d11 100644 --- a/src/web/lisa-html-snapshot.test.ts +++ b/src/web/lisa-html-snapshot.test.ts @@ -16,11 +16,11 @@ import { MAIN_HTML } from "./lisa-html.js"; * change the GUI markup/CSS/JS, recompute them: * node --import tsx -e 'import("./src/web/lisa-html.ts").then(async m=>{const {createHash}=await import("node:crypto");console.log(m.MAIN_HTML.length, createHash("sha256").update(m.MAIN_HTML).digest("hex"))})' * - * Last updated: rich multi-agent sidebar (agents header, per-row activity line via sbActivity). + * Last updated: managed-agent controls (delegate form + per-row approve/deny/send/cancel). */ -const EXPECTED_LENGTH = 79655; +const EXPECTED_LENGTH = 84648; const EXPECTED_SHA256 = - "ab91c1b4fbb085b2dbdea9c389cfd1a4780613226921ccfdf6f70d2d77d5948a"; + "e6ec7da0e4d36c462839c4078a4588735c18d718d71ff421ea6607087e2b924d"; test("MAIN_HTML length is byte-identical to the pre-split snapshot", () => { assert.equal(MAIN_HTML.length, EXPECTED_LENGTH); diff --git a/src/web/lisa-html.ts b/src/web/lisa-html.ts index 9925677..1fd7fd3 100644 --- a/src/web/lisa-html.ts +++ b/src/web/lisa-html.ts @@ -80,6 +80,10 @@ ${MAIN_CSS}