wisp-agents 0.15.0 → 0.17.0
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.
- package/CHANGELOG.md +55 -1
- package/README.md +29 -13
- package/bin/wisp.js +3 -2
- package/package.json +3 -3
- package/server/adapters/antigravity.js +7 -3
- package/server/adapters/claude.js +79 -55
- package/server/adapters/codex.js +5 -2
- package/server/adapters/common.js +14 -34
- package/server/adapters/cursor.js +4 -2
- package/server/adapters/grok.js +17 -2
- package/server/adapters/index.js +25 -2
- package/server/adapters/opencode.js +78 -12
- package/server/checkpoint.js +12 -68
- package/server/client.js +6 -4
- package/server/git.js +9 -4
- package/server/handoff.js +71 -0
- package/server/http/respond.js +59 -0
- package/server/http/router.js +64 -0
- package/server/http/static.js +36 -0
- package/server/index.js +138 -1216
- package/server/machines/bootstrap.js +56 -0
- package/server/machines/hosts.js +127 -0
- package/server/machines.js +7 -179
- package/server/mcp-ask.js +113 -8
- package/server/mcp-wisp.js +17 -5
- package/server/mentions.js +39 -0
- package/server/openapi.js +36 -4
- package/server/paths.js +32 -0
- package/server/preferences.js +2 -6
- package/server/proc.js +54 -0
- package/server/protocol.js +8 -1
- package/server/recipes.js +2 -4
- package/server/remote.js +14 -14
- package/server/render.js +3 -1
- package/server/router.js +7 -3
- package/server/routes/auto.js +29 -0
- package/server/routes/automations.js +92 -0
- package/server/routes/internal.js +176 -0
- package/server/routes/machines.js +43 -0
- package/server/routes/meta.js +76 -0
- package/server/routes/proxy.js +47 -0
- package/server/routes/sessions.js +351 -0
- package/server/routes/settings.js +144 -0
- package/server/routes/ship.js +61 -0
- package/server/routes/uploads.js +19 -0
- package/server/runner.js +216 -49
- package/server/services/auto.js +67 -0
- package/server/services/feedback.js +53 -0
- package/server/services/install.js +69 -0
- package/server/services/launch.js +127 -0
- package/server/services/phone.js +65 -0
- package/server/services/ship.js +82 -0
- package/server/services/telemetry.js +34 -0
- package/server/services/uploads.js +59 -0
- package/server/services/webhooks.js +28 -0
- package/server/store.js +13 -5
- package/server/subagents.js +5 -4
- package/server/telemetry.js +1 -1
- package/server/terminal.js +2 -1
- package/server/triggers.js +5 -7
- package/server/update.js +4 -3
- package/server/usage.js +94 -0
- package/server/util.js +42 -0
- package/server/welcome.js +3 -3
- package/web/css/auto.css +29 -0
- package/web/css/automations.css +71 -0
- package/web/css/base.css +83 -0
- package/web/css/blocks.css +461 -0
- package/web/css/composer.css +84 -0
- package/web/css/feed.css +235 -0
- package/web/css/launcher.css +204 -0
- package/web/css/layout.css +61 -0
- package/web/css/machines.css +125 -0
- package/web/css/modals.css +176 -0
- package/web/css/palette.css +79 -0
- package/web/css/responsive.css +35 -0
- package/web/css/ship.css +95 -0
- package/web/css/subagents.css +130 -0
- package/web/css/tokens.css +115 -0
- package/web/index.html +17 -1
- package/web/js/app.js +28 -5237
- package/web/js/blocks.js +46 -0
- package/web/js/core/api.js +25 -0
- package/web/js/core/bus.js +28 -0
- package/web/js/core/dom.js +7 -0
- package/web/js/core/format.js +27 -0
- package/web/js/core/state.js +43 -0
- package/web/js/features/actions.js +344 -0
- package/web/js/features/attachments.js +115 -0
- package/web/js/features/auto.js +54 -0
- package/web/js/features/automations.js +305 -0
- package/web/js/features/commands.js +123 -0
- package/web/js/features/compare.js +105 -0
- package/web/js/features/composer.js +315 -0
- package/web/js/features/feed.js +400 -0
- package/web/js/features/feedback.js +127 -0
- package/web/js/features/keys.js +83 -0
- package/web/js/features/launcher.js +561 -0
- package/web/js/features/live.js +91 -0
- package/web/js/features/machines.js +313 -0
- package/web/js/features/memory.js +154 -0
- package/web/js/features/mentions.js +125 -0
- package/web/js/features/notify.js +36 -0
- package/web/js/features/phone.js +124 -0
- package/web/js/features/places.js +38 -0
- package/web/js/features/projects.js +284 -0
- package/web/js/features/runner-picker.js +233 -0
- package/web/js/features/runners.js +163 -0
- package/web/js/features/scroll.js +75 -0
- package/web/js/features/settings.js +390 -0
- package/web/js/features/setup.js +94 -0
- package/web/js/features/ship.js +122 -0
- package/web/js/features/team.js +386 -0
- package/web/js/features/turns.js +432 -0
- package/web/js/features/update.js +48 -0
- package/web/js/features/usage.js +75 -0
- package/web/js/mention-text.js +59 -0
- package/web/js/ui/kbd.js +46 -0
- package/web/js/ui/overlay.js +38 -0
- package/web/js/ui/popover.js +52 -0
- package/web/js/ui/toast.js +23 -0
- package/web/css/app.css +0 -1910
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// Subagents: a team the lead can delegate to, any harness, model and thinking level per member, plus
|
|
2
|
+
// the user's "use it for" note. Picked in the launcher, saved as presets, shown on cards and turns.
|
|
3
|
+
|
|
4
|
+
import { fmtDuration, renderPresentation } from '../blocks.js';
|
|
5
|
+
import { esc } from '../md.js';
|
|
6
|
+
import { api } from '../core/api.js';
|
|
7
|
+
import { $, $$, el, icon } from '../core/dom.js';
|
|
8
|
+
import { STATUS_LABEL } from '../core/format.js';
|
|
9
|
+
import { S } from '../core/state.js';
|
|
10
|
+
import { popover } from '../ui/popover.js';
|
|
11
|
+
import { toast, toastError, tryApi } from '../ui/toast.js';
|
|
12
|
+
import { draftInto } from './composer.js';
|
|
13
|
+
import { scheduleRefetch } from './feed.js';
|
|
14
|
+
import { runnerPicker } from './runner-picker.js';
|
|
15
|
+
import { agentById, agentName, effortLabel, harnessesAt, harnessLogo, paintRunnerBtn, savedEffort, savedModel } from './runners.js';
|
|
16
|
+
import { ctxFor } from './turns.js';
|
|
17
|
+
|
|
18
|
+
const MAX_TEAM = 6; // rows
|
|
19
|
+
const MAX_COPIES = 5; // parallel copies of one row
|
|
20
|
+
const MAX_INSTANCES = 12; // copies across the team
|
|
21
|
+
export const copiesOf = (m) => Math.max(1, m.count || 1);
|
|
22
|
+
export const teamSize = (team) => team.reduce((n, m) => n + copiesOf(m), 0);
|
|
23
|
+
export const canLead = (id) => agentById(id)?.delegates !== false;
|
|
24
|
+
export const memberLabel = (m) => {
|
|
25
|
+
const a = agentById(m.agent);
|
|
26
|
+
const model = (m.model && a?.models?.find((x) => x.id === m.model)?.label) || m.model || 'Default model';
|
|
27
|
+
return `${a?.name || agentName(m.agent)} · ${model}${m.effort ? ` · ${effortLabel(m.effort)}` : ''}`;
|
|
28
|
+
};
|
|
29
|
+
const slugName = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24);
|
|
30
|
+
|
|
31
|
+
// Same rule as the server: blank names become the harness id, repeats get -2, -3…
|
|
32
|
+
function uniqueNames(members) {
|
|
33
|
+
const taken = new Set();
|
|
34
|
+
return members.map((m) => {
|
|
35
|
+
const base = slugName(m.name) || m.agent;
|
|
36
|
+
let name = base;
|
|
37
|
+
for (let i = 2; taken.has(name); i++) name = `${base}-${i}`;
|
|
38
|
+
taken.add(name);
|
|
39
|
+
return { ...m, name };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The lead's logo with its subagents' tucked behind it; they fan out on hover. A member that
|
|
44
|
+
// is working pulses; one the lead never used is dimmed.
|
|
45
|
+
export function teamStack(lead, team) {
|
|
46
|
+
const shown = team.slice(0, 4);
|
|
47
|
+
const tip = `${agentName(lead)} with ${team.map((m) => `${m.name}${m.busy ? ' (working)' : m.used === false ? ' (unused)' : ''}`).join(', ')}`;
|
|
48
|
+
return `<span class="team-stack" data-tip="${esc(tip)}" aria-label="${esc(tip)}" tabindex="0">
|
|
49
|
+
<span class="ts-l">${harnessLogo(lead)}</span>
|
|
50
|
+
${shown.map((m, i) => `<span class="ts-m${m.used === false ? ' idle' : ''}${m.busy ? ' busy' : ''}" style="--i:${i}">${harnessLogo(m.agent)}</span>`).join('')}
|
|
51
|
+
${team.length > shown.length ? `<span class="ts-m ts-more" style="--i:${shown.length}">+${team.length - shown.length}</span>` : ''}
|
|
52
|
+
</span>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Launcher button: a stack of the members' logos and the preset's name, or just "Subagents".
|
|
56
|
+
export function paintTeamBtn(btn, team, presetId, lead) {
|
|
57
|
+
const ok = canLead(lead);
|
|
58
|
+
const flat = team.flatMap((m) => Array(copiesOf(m)).fill(m));
|
|
59
|
+
const n = flat.length;
|
|
60
|
+
const p = (S.config.teams || []).find((x) => x.id === presetId);
|
|
61
|
+
btn.classList.toggle('on', team.length > 0 && ok);
|
|
62
|
+
btn.classList.toggle('off', team.length > 0 && !ok);
|
|
63
|
+
btn.innerHTML = team.length
|
|
64
|
+
? `<span class="tb-stack">${flat.slice(0, 5).map((m, i) => `<span class="ts-m" style="--i:${i}">${harnessLogo(m.agent)}</span>`).join('')}${n > 5 ? `<span class="ts-m ts-more" style="--i:5">+${n - 5}</span>` : ''}</span><span class="tb-label">${esc(p?.name || `${n} subagent${n > 1 ? 's' : ''}`)}</span>`
|
|
65
|
+
: `${icon('agent')}<span class="tb-label">Subagents</span>`;
|
|
66
|
+
btn.title = !team.length
|
|
67
|
+
? 'Subagents: give the lead a team on other harnesses to delegate to'
|
|
68
|
+
: ok
|
|
69
|
+
? `Subagents the lead can delegate to:\n${team.map((m) => `• ${m.name}${copiesOf(m) > 1 ? ` ×${copiesOf(m)}` : ''}: ${memberLabel(m)}${m.use ? ` — ${m.use}` : ''}`).join('\n')}`
|
|
70
|
+
: `${agentName(lead)} can't delegate (wisp can't give it tools), so this launch runs without subagents`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function teamEditor(anchor, { lead, team, presetId, places }, onChange) {
|
|
74
|
+
const open = $('.team-pop');
|
|
75
|
+
if (open) return open.close();
|
|
76
|
+
let members = team.map((m) => ({ ...m }));
|
|
77
|
+
let preset = presetId;
|
|
78
|
+
const r = anchor.getBoundingClientRect();
|
|
79
|
+
const width = Math.min(540, innerWidth - 24);
|
|
80
|
+
const left = Math.max(12, Math.min(r.left, innerWidth - 12 - width));
|
|
81
|
+
const below = r.bottom + 440 < innerHeight;
|
|
82
|
+
const node = el(`
|
|
83
|
+
<div class="team-pop" role="dialog" aria-label="Subagents" style="left:${left}px;width:${width}px;${below ? `top:${r.bottom + 6}px` : `bottom:${innerHeight - r.top + 6}px`}">
|
|
84
|
+
<header class="tp-head">
|
|
85
|
+
<div class="tp-title"><b>Subagents</b><small>The lead hands tasks to these, each on its own harness. Tell it what each one is for.</small></div>
|
|
86
|
+
<button class="ghost xs tp-presets" aria-haspopup="menu">${icon('agent')}<span>Presets</span>${icon('chevron', 'chev')}</button>
|
|
87
|
+
</header>
|
|
88
|
+
<div class="tp-warn" hidden></div>
|
|
89
|
+
<div class="tp-list"></div>
|
|
90
|
+
<footer class="tp-foot"></footer>
|
|
91
|
+
</div>`);
|
|
92
|
+
const list = $('.tp-list', node);
|
|
93
|
+
const foot = $('.tp-foot', node);
|
|
94
|
+
const commit = () => onChange(uniqueNames(members), preset);
|
|
95
|
+
let typing;
|
|
96
|
+
const commitSoon = () => (clearTimeout(typing), (typing = setTimeout(commit, 300)));
|
|
97
|
+
|
|
98
|
+
const paintWarn = () => {
|
|
99
|
+
const w = $('.tp-warn', node);
|
|
100
|
+
w.hidden = canLead(lead);
|
|
101
|
+
w.innerHTML = `${icon('warn')}<span>${esc(agentName(lead))} can't delegate: wisp can't give it tools. Pick Claude Code, Codex or opencode as the lead to use these.</span>`;
|
|
102
|
+
};
|
|
103
|
+
const paintFoot = () => {
|
|
104
|
+
foot.innerHTML = `
|
|
105
|
+
<button class="ghost sm tp-add" ${members.length >= MAX_TEAM || teamSize(members) >= MAX_INSTANCES ? 'disabled' : ''}>${icon('plus')}Add subagent</button>
|
|
106
|
+
<span class="tp-count" title="Agents in all, counting copies (max ${MAX_INSTANCES})">${teamSize(members)} agent${teamSize(members) === 1 ? '' : 's'}</span>
|
|
107
|
+
<span class="grow"></span>
|
|
108
|
+
${members.length ? `<button class="link xs tp-clear">Clear</button><button class="ghost sm tp-save">${icon('check')}Save as preset</button>` : ''}`;
|
|
109
|
+
};
|
|
110
|
+
const paintList = () => {
|
|
111
|
+
const named = uniqueNames(members);
|
|
112
|
+
list.innerHTML = members.length
|
|
113
|
+
? members
|
|
114
|
+
.map(
|
|
115
|
+
(m, i) => `<div class="tp-row" data-i="${i}">
|
|
116
|
+
<div class="tp-top">
|
|
117
|
+
<button class="runner-btn tp-runner" title="Harness, model and thinking level" aria-haspopup="dialog"></button>
|
|
118
|
+
<span class="tp-copies${copiesOf(m) > 1 ? ' on' : ''}" title="Copies: identical agents that take tasks in parallel">
|
|
119
|
+
<button class="tp-dec" aria-label="One copy fewer" ${copiesOf(m) <= 1 ? 'disabled' : ''}>${icon('minus')}</button>
|
|
120
|
+
<b>×${copiesOf(m)}</b>
|
|
121
|
+
<button class="tp-inc" aria-label="One copy more" ${copiesOf(m) >= MAX_COPIES || teamSize(members) >= MAX_INSTANCES ? 'disabled' : ''}>${icon('plus')}</button>
|
|
122
|
+
</span>
|
|
123
|
+
<label class="field sm mono tp-name" title="What the lead calls it"><span>@</span><input value="${esc(m.name || '')}" placeholder="${esc(named[i].name)}" maxlength="24" spellcheck="false" autocomplete="off"></label>
|
|
124
|
+
<button class="icon-btn tp-del" title="Remove">${icon('x')}</button>
|
|
125
|
+
</div>
|
|
126
|
+
<label class="field sm tp-use"><span>Use for</span><input value="${esc(m.use || '')}" placeholder="e.g. image and UI work · research · writing tests" maxlength="300" spellcheck="false" autocomplete="off"></label>
|
|
127
|
+
</div>`,
|
|
128
|
+
)
|
|
129
|
+
.join('')
|
|
130
|
+
: `<div class="tp-empty">${icon('agent')}<p>No subagents yet. Add one per harness you want the lead to use, like Codex for tests or opencode for research.</p></div>`;
|
|
131
|
+
$$('.tp-row', list).forEach((row, i) => paintRunnerBtn($('.tp-runner', row), members[i].agent, members[i].model, members[i].effort));
|
|
132
|
+
paintFoot();
|
|
133
|
+
};
|
|
134
|
+
const add = () => {
|
|
135
|
+
if (members.length >= MAX_TEAM || teamSize(members) >= MAX_INSTANCES) return;
|
|
136
|
+
const used = new Set([lead, ...members.map((m) => m.agent)]);
|
|
137
|
+
const pool = places ? harnessesAt(places) : S.agents;
|
|
138
|
+
const a = pool.find((x) => x.available && !used.has(x.id)) || pool.find((x) => x.available && x.id !== lead) || pool.find((x) => x.available);
|
|
139
|
+
if (!a) return toast('No harness is installed.', { tone: 'bad' });
|
|
140
|
+
const model = savedModel(a.id);
|
|
141
|
+
members.push({ name: '', agent: a.id, model, effort: savedEffort(a.id, model), use: '', count: 1 });
|
|
142
|
+
paintList();
|
|
143
|
+
commit();
|
|
144
|
+
$(`.tp-row[data-i="${members.length - 1}"] .tp-use input`, list)?.focus();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Save: name it; an existing preset's name updates that preset.
|
|
148
|
+
const askName = () => {
|
|
149
|
+
const cur = (S.config.teams || []).find((x) => x.id === preset);
|
|
150
|
+
foot.innerHTML = `<form class="tp-saveform"><input class="input sm" placeholder="Preset name, e.g. Review crew" value="${esc(cur?.name || '')}" maxlength="60"><button class="primary sm">Save</button><button type="button" class="link xs tp-cancel">Cancel</button></form>`;
|
|
151
|
+
const input = $('input', foot);
|
|
152
|
+
input.focus();
|
|
153
|
+
input.select();
|
|
154
|
+
$('.tp-cancel', foot).onclick = paintFoot;
|
|
155
|
+
$('form', foot).onsubmit = async (e) => {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
const name = input.value.trim();
|
|
158
|
+
if (!name) return input.focus();
|
|
159
|
+
const same = (S.config.teams || []).find((x) => x.name.toLowerCase() === name.toLowerCase());
|
|
160
|
+
try {
|
|
161
|
+
const p = await api(same ? `/teams/${same.id}` : '/teams', { method: same ? 'PATCH' : 'POST', body: { name, members: uniqueNames(members) } });
|
|
162
|
+
S.config.teams = [...(S.config.teams || []).filter((x) => x.id !== p.id), p];
|
|
163
|
+
preset = p.id;
|
|
164
|
+
commit();
|
|
165
|
+
paintFoot();
|
|
166
|
+
toast(`${same ? 'Updated' : 'Saved'} preset <b>${esc(p.name)}</b>`);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
toastError(err);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const presetMenu = (btn) => {
|
|
174
|
+
const prev = $('.menu.tp-menu');
|
|
175
|
+
if (prev) return prev.close();
|
|
176
|
+
const teams = S.config.teams || [];
|
|
177
|
+
const b = btn.getBoundingClientRect();
|
|
178
|
+
const menu = el(`
|
|
179
|
+
<div class="menu tp-menu" role="menu" style="top:${b.bottom + 4}px;right:${innerWidth - b.right}px">
|
|
180
|
+
${teams.length
|
|
181
|
+
? teams
|
|
182
|
+
.map(
|
|
183
|
+
(t) => `<button role="menuitem" data-id="${esc(t.id)}" class="${t.id === preset ? 'on' : ''}">
|
|
184
|
+
<span class="tb-stack">${t.members.slice(0, 4).map((m, i) => `<span class="ts-m" style="--i:${i}">${harnessLogo(m.agent)}</span>`).join('')}</span>
|
|
185
|
+
<span class="mm-text"><b>${esc(t.name)}</b><small>${esc(t.members.map((m) => m.name).join(', '))}</small></span>
|
|
186
|
+
<span class="tp-mdel" data-del="${esc(t.id)}" title="Delete preset">${icon('trash')}</span>
|
|
187
|
+
</button>`,
|
|
188
|
+
)
|
|
189
|
+
.join('')
|
|
190
|
+
: `<div class="tp-mempty">No presets yet. Set up a team, then <b>Save as preset</b>.</div>`}
|
|
191
|
+
</div>`);
|
|
192
|
+
menu.onclick = async (ev) => {
|
|
193
|
+
const del = ev.target.closest('[data-del]');
|
|
194
|
+
if (del) {
|
|
195
|
+
ev.stopPropagation();
|
|
196
|
+
const t = teams.find((x) => x.id === del.dataset.del);
|
|
197
|
+
if (!(await tryApi(() => api(`/teams/${t.id}`, { method: 'DELETE' })))) return close();
|
|
198
|
+
S.config.teams = (S.config.teams || []).filter((x) => x.id !== t.id);
|
|
199
|
+
if (preset === t.id) (preset = ''), commit();
|
|
200
|
+
close();
|
|
201
|
+
return toast(`Deleted preset <b>${esc(t.name)}</b>`);
|
|
202
|
+
}
|
|
203
|
+
const item = ev.target.closest('button[data-id]');
|
|
204
|
+
if (!item) return;
|
|
205
|
+
const t = teams.find((x) => x.id === item.dataset.id);
|
|
206
|
+
members = t.members.map((m) => ({ ...m }));
|
|
207
|
+
preset = t.id;
|
|
208
|
+
close();
|
|
209
|
+
paintList();
|
|
210
|
+
commit();
|
|
211
|
+
};
|
|
212
|
+
const close = popover(menu, { anchor: btn, capture: true });
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
list.addEventListener('click', (e) => {
|
|
216
|
+
const row = e.target.closest('.tp-row');
|
|
217
|
+
if (!row) return;
|
|
218
|
+
const i = Number(row.dataset.i);
|
|
219
|
+
if (e.target.closest('.tp-del')) {
|
|
220
|
+
members.splice(i, 1);
|
|
221
|
+
paintList();
|
|
222
|
+
return commit();
|
|
223
|
+
}
|
|
224
|
+
const step = e.target.closest('.tp-inc') ? 1 : e.target.closest('.tp-dec') ? -1 : 0;
|
|
225
|
+
if (step) {
|
|
226
|
+
const m = members[i];
|
|
227
|
+
const next = copiesOf(m) + step;
|
|
228
|
+
if (next < 1 || next > MAX_COPIES || (step > 0 && teamSize(members) >= MAX_INSTANCES)) return;
|
|
229
|
+
m.count = next;
|
|
230
|
+
paintList();
|
|
231
|
+
$(`.tp-row[data-i="${i}"] ${step > 0 ? '.tp-inc' : '.tp-dec'}`, list)?.focus();
|
|
232
|
+
return commit();
|
|
233
|
+
}
|
|
234
|
+
const rb = e.target.closest('.tp-runner');
|
|
235
|
+
if (rb) {
|
|
236
|
+
const m = members[i];
|
|
237
|
+
runnerPicker(rb, { agent: m.agent, model: m.model, effort: m.effort, savedModel, savedEffort, places, auto: true }, (agent, model, effort) => {
|
|
238
|
+
Object.assign(m, { agent, model, effort });
|
|
239
|
+
paintList();
|
|
240
|
+
commit();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
list.addEventListener('input', (e) => {
|
|
245
|
+
const row = e.target.closest('.tp-row');
|
|
246
|
+
if (!row) return;
|
|
247
|
+
const m = members[Number(row.dataset.i)];
|
|
248
|
+
if (e.target.closest('.tp-name')) m.name = e.target.value;
|
|
249
|
+
else m.use = e.target.value;
|
|
250
|
+
commitSoon();
|
|
251
|
+
});
|
|
252
|
+
foot.addEventListener('click', (e) => {
|
|
253
|
+
if (e.target.closest('.tp-add')) add();
|
|
254
|
+
if (e.target.closest('.tp-save')) askName();
|
|
255
|
+
if (e.target.closest('.tp-clear')) {
|
|
256
|
+
members = [];
|
|
257
|
+
preset = '';
|
|
258
|
+
paintList();
|
|
259
|
+
commit();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
$('.tp-presets', node).onclick = (e) => presetMenu(e.currentTarget);
|
|
263
|
+
|
|
264
|
+
paintWarn();
|
|
265
|
+
paintList();
|
|
266
|
+
// The harness picker and preset menu float outside the popover; clicks there keep it open.
|
|
267
|
+
const close = popover(node, {
|
|
268
|
+
anchor,
|
|
269
|
+
keep: '.picker, .tp-menu',
|
|
270
|
+
onClose: () => {
|
|
271
|
+
clearTimeout(typing);
|
|
272
|
+
commit();
|
|
273
|
+
$('.menu.tp-menu')?.close();
|
|
274
|
+
anchor.classList.remove('open');
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
anchor.classList.add('open');
|
|
278
|
+
if (!members.length) $('.tp-add', node)?.focus();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// One delegation with the child session and turn it ran as (from the lead's childSessions).
|
|
282
|
+
function delegationsOf(s, t) {
|
|
283
|
+
return (t.delegations || []).map((d) => {
|
|
284
|
+
const child = s.childSessions?.[d.sessionId];
|
|
285
|
+
return { d, child, ct: child?.turns.find((x) => x.id === d.turnId) };
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
export const subKey = (s, t) => delegationsOf(s, t).map(({ ct }) => ct?.status || '?').join(',');
|
|
289
|
+
const subStatus = (ct) => {
|
|
290
|
+
if (!ct || ct.status === 'running') return { cls: 'waiting', label: 'Working' };
|
|
291
|
+
if (ct.status === 'stopped') return { cls: 'partial', label: 'Stopped' };
|
|
292
|
+
if (ct.status === 'error') return { cls: 'blocked', label: 'Failed' };
|
|
293
|
+
const st = ct.presentation?.blocks?.find((b) => b.type === 'hero')?.status || 'done';
|
|
294
|
+
return { cls: st, label: STATUS_LABEL[st] || 'Done' };
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// While the lead works: one live line per subagent it's waiting on.
|
|
298
|
+
export function teamLiveHtml(s, t) {
|
|
299
|
+
const list = delegationsOf(s, t);
|
|
300
|
+
if (!list.length) return '';
|
|
301
|
+
return `<div class="team-live">${list
|
|
302
|
+
.map(({ d, ct }) => {
|
|
303
|
+
const running = !ct || ct.status === 'running';
|
|
304
|
+
const hero = ct?.presentation?.blocks?.find((b) => b.type === 'hero');
|
|
305
|
+
const last = ct?.activity?.findLast((a) => a.kind !== 'text')?.label;
|
|
306
|
+
const st = subStatus(ct);
|
|
307
|
+
return `<div class="tl-row ${running ? 'live' : `st-${st.cls}`}" data-sub="${esc(d.sessionId)}">
|
|
308
|
+
<span class="tl-logo">${harnessLogo(d.agent)}</span>
|
|
309
|
+
<b class="tl-name">@${esc(d.name)}</b>
|
|
310
|
+
<span class="tl-live">${esc(running ? last || 'Starting up' : hero?.title || st.label)}</span>
|
|
311
|
+
${running ? `<span class="elapsed" data-start="${ct?.startedAt || d.startedAt}">${fmtDuration(Date.now() - (ct?.startedAt || d.startedAt))}</span>` : `<span class="pill st-${st.cls}"><i></i>${st.label}</span>`}
|
|
312
|
+
</div>`;
|
|
313
|
+
})
|
|
314
|
+
.join('')}</div>`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// After the run: the team's reports, one card each, opening into the subagent's own presentation.
|
|
318
|
+
const subOpen = new Set(); // delegation ids the user unfolded
|
|
319
|
+
export function renderSubs(s, t) {
|
|
320
|
+
const list = delegationsOf(s, t);
|
|
321
|
+
if (!list.length) return null;
|
|
322
|
+
const names = [...new Set(list.map(({ d }) => `@${d.name}`))];
|
|
323
|
+
const node = el(`
|
|
324
|
+
<section class="subs">
|
|
325
|
+
<div class="subs-head">
|
|
326
|
+
${teamStack(s.agent, list.map(({ d, ct }) => ({ name: d.name, agent: d.agent, busy: ct?.status === 'running' })))}
|
|
327
|
+
<b>${list.length} delegated task${list.length > 1 ? 's' : ''}</b>
|
|
328
|
+
<span class="subs-to">${esc(names.join(', '))}</span>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="subs-list"></div>
|
|
331
|
+
</section>`);
|
|
332
|
+
const box = $('.subs-list', node);
|
|
333
|
+
for (const { d, child, ct } of list) {
|
|
334
|
+
const st = subStatus(ct);
|
|
335
|
+
const hero = ct?.presentation?.blocks?.find((b) => b.type === 'hero');
|
|
336
|
+
const files = ct?.changes?.length || 0;
|
|
337
|
+
const card = el(`
|
|
338
|
+
<article class="sub st-${st.cls} ${subOpen.has(d.id) ? 'open' : ''}">
|
|
339
|
+
<button class="sub-head" aria-expanded="${subOpen.has(d.id)}">
|
|
340
|
+
<span class="sub-logo">${harnessLogo(d.agent)}</span>
|
|
341
|
+
<span class="sub-main">
|
|
342
|
+
<span class="sub-top"><b>@${esc(d.name)}</b><span class="sub-model">${esc(memberLabel(d))}</span></span>
|
|
343
|
+
<span class="sub-title">${esc(hero?.title || (ct?.status === 'running' ? ct.activity?.findLast((a) => a.kind !== 'text')?.label || 'Working' : d.prompt))}</span>
|
|
344
|
+
</span>
|
|
345
|
+
<span class="sub-side">
|
|
346
|
+
${files ? `<span class="sub-files">${files} file${files > 1 ? 's' : ''}</span>` : ''}
|
|
347
|
+
${ct?.endedAt ? `<span class="sub-dur">${fmtDuration(ct.endedAt - ct.startedAt)}</span>` : ''}
|
|
348
|
+
<span class="pill st-${st.cls}"><i></i>${st.label}</span>
|
|
349
|
+
${icon('chevron', 'chev')}
|
|
350
|
+
</span>
|
|
351
|
+
</button>
|
|
352
|
+
<div class="sub-body" ${subOpen.has(d.id) ? '' : 'hidden'}></div>
|
|
353
|
+
</article>`);
|
|
354
|
+
const body = $('.sub-body', card);
|
|
355
|
+
const fill = () => {
|
|
356
|
+
body.innerHTML = `<div class="sub-task"><span>Task from ${esc(agentName(s.agent))}</span><p>${esc(d.prompt)}</p></div>`;
|
|
357
|
+
if (ct?.presentation && child) {
|
|
358
|
+
// Its "next" and question blocks draft into the lead's composer: the lead stays in charge.
|
|
359
|
+
const ctx = { ...ctxFor(child, ct, false), reply: (text) => draftInto(s.id, `Ask @${d.name} to: ${text}`), draft: (text) => draftInto(s.id, `Ask @${d.name} to: ${text}`) };
|
|
360
|
+
body.append(renderPresentation(ct.presentation, ctx));
|
|
361
|
+
} else if (ct?.status === 'running') body.append(el(`<p class="muted sub-wait">Still working…</p>`));
|
|
362
|
+
};
|
|
363
|
+
if (subOpen.has(d.id)) fill();
|
|
364
|
+
$('.sub-head', card).onclick = () => {
|
|
365
|
+
const opening = body.hidden;
|
|
366
|
+
opening ? subOpen.add(d.id) : subOpen.delete(d.id);
|
|
367
|
+
card.classList.toggle('open', opening);
|
|
368
|
+
$('.sub-head', card).setAttribute('aria-expanded', String(opening));
|
|
369
|
+
if (opening) fill();
|
|
370
|
+
body.hidden = !opening;
|
|
371
|
+
};
|
|
372
|
+
box.append(card);
|
|
373
|
+
}
|
|
374
|
+
return node;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// A subagent's summary: its lead's card shows it, so update the live line or re-fetch the lead.
|
|
378
|
+
const childState = new Map();
|
|
379
|
+
export function onChildSummary(c) {
|
|
380
|
+
const row = $(`.tl-row[data-sub="${CSS.escape(c.id)}"] .tl-live`);
|
|
381
|
+
if (row && c.state === 'running' && c.live) row.textContent = c.live;
|
|
382
|
+
if (childState.get(c.id) !== c.state) {
|
|
383
|
+
childState.set(c.id, c.state);
|
|
384
|
+
scheduleRefetch(c.parentId);
|
|
385
|
+
}
|
|
386
|
+
}
|