Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/web/lisa-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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');
Expand Down
57 changes: 57 additions & 0 deletions src/web/lisa-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/web/lisa-html-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/web/lisa-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ ${MAIN_CSS}
<div class="left">agents</div>
<div class="count">▶︎ <span id="sbClaudeCount">0</span></div>
</div>
<form id="sbDelegate" class="delegate" autocomplete="off">
<input id="sbDelegateTask" type="text" placeholder="delegate a task to a managed agent…" />
<button type="submit" title="Start a managed agent">▶</button>
</form>
<div id="sbClaudeRows">
<div class="session-empty">(idle)</div>
</div>
Expand Down