wisp-agents 0.5.0 → 0.7.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 +36 -1
- package/README.md +71 -9
- package/bin/wisp.js +182 -69
- package/package.json +15 -4
- package/server/adapters/antigravity.js +155 -0
- package/server/adapters/index.js +6 -2
- package/server/client.js +177 -0
- package/server/index.js +157 -1
- package/server/mcp-wisp.js +164 -0
- package/server/openapi.js +204 -0
- package/server/preferences.js +296 -0
- package/server/render.js +100 -0
- package/server/runner.js +2 -1
- package/server/update.js +136 -0
- package/skills/wisp/SKILL.md +55 -0
- package/web/css/app.css +83 -1
- package/web/index.html +5 -0
- package/web/js/app.js +369 -23
- package/web/logos/antigravity-color.svg +1 -0
package/web/js/app.js
CHANGED
|
@@ -67,6 +67,7 @@ const HARNESS_LOGO = {
|
|
|
67
67
|
codex: { file: 'codex-color' },
|
|
68
68
|
opencode: { file: 'opencode', mono: true },
|
|
69
69
|
cursor: { file: 'cursor', mono: true },
|
|
70
|
+
antigravity: { file: 'antigravity-color' },
|
|
70
71
|
};
|
|
71
72
|
function harnessLogo(agent) {
|
|
72
73
|
const l = HARNESS_LOGO[agent];
|
|
@@ -196,7 +197,29 @@ function renderFeedHead(needs, working) {
|
|
|
196
197
|
// never shifts the feed, and it stays reachable deep inside a long open card.
|
|
197
198
|
const collapseAllBtn = el(`<button class="collapse-all" title="Collapse all (C) · Esc collapses the focused one"><span></span><kbd>C</kbd></button>`);
|
|
198
199
|
document.body.append(collapseAllBtn);
|
|
199
|
-
|
|
200
|
+
// Collapsing shrinks everything above the reader, which would leave them staring
|
|
201
|
+
// at unrelated cards far down the feed. Keep the card they were reading in view.
|
|
202
|
+
const collapseAll = () => {
|
|
203
|
+
const anchor = readingCard();
|
|
204
|
+
[...S.open].forEach((id) => collapse(id));
|
|
205
|
+
if (!anchor) return;
|
|
206
|
+
setFocus(anchor, { scroll: false });
|
|
207
|
+
const card = S.nodes.get(anchor)?.card;
|
|
208
|
+
if (card?.isConnected) card.scrollIntoView({ block: 'center' });
|
|
209
|
+
};
|
|
210
|
+
// The open card covering the most of the viewport; the focused one breaks ties.
|
|
211
|
+
function readingCard() {
|
|
212
|
+
let best = null;
|
|
213
|
+
let bestArea = 0;
|
|
214
|
+
for (const id of S.open) {
|
|
215
|
+
const card = S.nodes.get(id)?.card;
|
|
216
|
+
if (!card?.isConnected) continue;
|
|
217
|
+
const r = card.getBoundingClientRect();
|
|
218
|
+
const seen = Math.min(r.bottom, innerHeight) - Math.max(r.top, 0) + (id === S.focus ? 1 : 0);
|
|
219
|
+
if (seen > bestArea) (best = id), (bestArea = seen);
|
|
220
|
+
}
|
|
221
|
+
return best || (S.open.has(S.focus) ? S.focus : null);
|
|
222
|
+
}
|
|
200
223
|
collapseAllBtn.addEventListener('click', collapseAll);
|
|
201
224
|
function placeCollapseAll() {
|
|
202
225
|
const open = [...S.open].filter((id) => S.nodes.get(id)?.card.isConnected).length;
|
|
@@ -433,6 +456,20 @@ function autosize(ta, max = 340) {
|
|
|
433
456
|
|
|
434
457
|
let launchProject = load('wisp.launchProject', null);
|
|
435
458
|
|
|
459
|
+
// The model is remembered per harness (ids aren't portable between them).
|
|
460
|
+
function savedModel(id) {
|
|
461
|
+
const d = S.config.defaults;
|
|
462
|
+
const want = d.models?.[id] ?? (id === d.agent ? d.model : '');
|
|
463
|
+
const models = S.agents.find((a) => a.id === id)?.models || [];
|
|
464
|
+
return models.some((m) => m.id === want) ? want : '';
|
|
465
|
+
}
|
|
466
|
+
// Thinking level too, and only if the chosen model still accepts it.
|
|
467
|
+
function savedEffort(id, model) {
|
|
468
|
+
const d = S.config.defaults;
|
|
469
|
+
const want = d.efforts?.[id] ?? (id === d.agent ? d.effort : '');
|
|
470
|
+
return effortsFor(id, model).includes(want) ? want : '';
|
|
471
|
+
}
|
|
472
|
+
|
|
436
473
|
function renderLauncher() {
|
|
437
474
|
const box = $('#launcher');
|
|
438
475
|
const mode = S.config.defaults.mode || 'edits';
|
|
@@ -485,19 +522,6 @@ function renderLauncher() {
|
|
|
485
522
|
};
|
|
486
523
|
paintMode();
|
|
487
524
|
modeBtn.onclick = () => modeMenu(modeBtn, curMode, setMode);
|
|
488
|
-
// The model is remembered per harness (ids aren't portable between them).
|
|
489
|
-
const savedModel = (id) => {
|
|
490
|
-
const d = S.config.defaults;
|
|
491
|
-
const want = d.models?.[id] ?? (id === d.agent ? d.model : '');
|
|
492
|
-
const models = S.agents.find((a) => a.id === id)?.models || [];
|
|
493
|
-
return models.some((m) => m.id === want) ? want : '';
|
|
494
|
-
};
|
|
495
|
-
// Thinking level too, and only if the chosen model still accepts it.
|
|
496
|
-
const savedEffort = (id, model) => {
|
|
497
|
-
const d = S.config.defaults;
|
|
498
|
-
const want = d.efforts?.[id] ?? (id === d.agent ? d.effort : '');
|
|
499
|
-
return effortsFor(id, model).includes(want) ? want : '';
|
|
500
|
-
};
|
|
501
525
|
let curModel = savedModel(agent?.id);
|
|
502
526
|
let curEffort = savedEffort(agent?.id, curModel);
|
|
503
527
|
const runnerBtn = $('#runner', box);
|
|
@@ -586,7 +610,7 @@ function modeMenu(anchor, current, pick) {
|
|
|
586
610
|
menu.addEventListener('keydown', (ev) => {
|
|
587
611
|
const items = $$('button', menu);
|
|
588
612
|
const i = items.indexOf(document.activeElement);
|
|
589
|
-
if (ev.key === 'Escape') (close(), anchor.focus());
|
|
613
|
+
if (ev.key === 'Escape') (ev.stopPropagation(), close(), anchor.focus());
|
|
590
614
|
else if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
|
|
591
615
|
ev.preventDefault();
|
|
592
616
|
items[(i + (ev.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length].focus();
|
|
@@ -1270,13 +1294,14 @@ function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort },
|
|
|
1270
1294
|
.map((e) => `<button role="radio" aria-checked="${e === current}" class="${e === current ? 'on' : ''}" data-e="${esc(e)}" title="${e ? `${esc(effortLabel(e))} reasoning effort` : 'Whatever the harness is set to'}">${esc(e ? effortLabel(e) : 'Default')}</button>`)
|
|
1271
1295
|
.join('')}</span>`;
|
|
1272
1296
|
};
|
|
1297
|
+
const markInUse = (id) => {
|
|
1298
|
+
$$('.pk-dot', node).forEach((d) => d.remove());
|
|
1299
|
+
sideRows.find((b) => b.dataset.id === id)?.insertAdjacentHTML('beforeend', '<span class="pk-dot" title="In use"></span>');
|
|
1300
|
+
};
|
|
1273
1301
|
const chooseEffort = (e) => {
|
|
1274
1302
|
const m = modelOf(focusAgent);
|
|
1275
1303
|
if (focusAgent === agent && e === effort) return;
|
|
1276
|
-
if (focusAgent !== agent)
|
|
1277
|
-
$$('.pk-dot', node).forEach((d) => d.remove());
|
|
1278
|
-
sideRows.find((b) => b.dataset.id === focusAgent)?.insertAdjacentHTML('beforeend', '<span class="pk-dot" title="In use"></span>');
|
|
1279
|
-
}
|
|
1304
|
+
if (focusAgent !== agent) markInUse(focusAgent);
|
|
1280
1305
|
agent = focusAgent;
|
|
1281
1306
|
model = m;
|
|
1282
1307
|
effort = e;
|
|
@@ -1336,6 +1361,17 @@ function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort },
|
|
|
1336
1361
|
if (focus) sideRows.find((b) => b.dataset.id === id)?.focus();
|
|
1337
1362
|
};
|
|
1338
1363
|
|
|
1364
|
+
// Picking a harness applies it straight away with the model and level it shows as selected.
|
|
1365
|
+
const useAgent = (id) => {
|
|
1366
|
+
preview(id);
|
|
1367
|
+
if (id === agent) return;
|
|
1368
|
+
markInUse(id);
|
|
1369
|
+
agent = id;
|
|
1370
|
+
model = savedModel(id);
|
|
1371
|
+
effort = savedEffort(id, model);
|
|
1372
|
+
pick(agent, model, effort);
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1339
1375
|
const close = () => {
|
|
1340
1376
|
node.remove();
|
|
1341
1377
|
anchor.classList.remove('open');
|
|
@@ -1390,6 +1426,7 @@ function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort },
|
|
|
1390
1426
|
preview(focusAgent, { focus: true });
|
|
1391
1427
|
} else if ((e.key === 'ArrowRight' || e.key === 'Enter') && inSide) {
|
|
1392
1428
|
e.preventDefault();
|
|
1429
|
+
if (e.key === 'Enter') useAgent(focusAgent);
|
|
1393
1430
|
focusModels();
|
|
1394
1431
|
} else if (e.key === 'Enter' && document.activeElement === input && rows()[0]) {
|
|
1395
1432
|
e.preventDefault();
|
|
@@ -1398,7 +1435,7 @@ function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort },
|
|
|
1398
1435
|
input.focus(); // type-to-search from anywhere in the picker
|
|
1399
1436
|
}
|
|
1400
1437
|
};
|
|
1401
|
-
sideRows.forEach((b) => (b.onclick = () =>
|
|
1438
|
+
sideRows.forEach((b) => (b.onclick = () => useAgent(b.dataset.id)));
|
|
1402
1439
|
list.onclick = (e) => {
|
|
1403
1440
|
const b = e.target.closest('.pk-m');
|
|
1404
1441
|
if (b) choose(b.dataset.id);
|
|
@@ -1459,6 +1496,7 @@ function taskMenu(id, e) {
|
|
|
1459
1496
|
<div class="menu task" style="top:${e.clientY}px;left:${e.clientX}px">
|
|
1460
1497
|
${S.open.has(id) ? `<button data-a="collapse">${icon('chevron')}Collapse${keyHint('O')}</button>` : `<button data-a="open">${icon('chevron')}Open${keyHint('O')}</button>`}
|
|
1461
1498
|
<button data-a="rename">${icon('pen')}Rename${keyHint('M')}</button>
|
|
1499
|
+
<button data-a="rerun">${icon('rerun')}Rerun…${keyHint('⇧R')}</button>
|
|
1462
1500
|
${busy || s.archived ? '' : `<button data-a="done">${icon('done')}${s.state === 'done' ? 'Reopen' : 'Mark done'}${keyHint('E')}</button>`}
|
|
1463
1501
|
${s.archived ? `<button data-a="unarchive">${icon('archive')}Unarchive${keyHint('A')}</button>` : busy ? '' : `<button data-a="archive">${icon('archive')}Archive${keyHint('A')}</button>`}
|
|
1464
1502
|
<button data-a="delete" class="danger">${icon('trash')}Delete session${keyHint('⌫')}</button>
|
|
@@ -1473,6 +1511,7 @@ function taskMenu(id, e) {
|
|
|
1473
1511
|
if (a === 'open') expand(id);
|
|
1474
1512
|
if (a === 'collapse') collapse(id);
|
|
1475
1513
|
if (a === 'rename') rename(id);
|
|
1514
|
+
if (a === 'rerun') rerun(id);
|
|
1476
1515
|
if (a === 'done') toggleDone(id);
|
|
1477
1516
|
if (a === 'archive' || a === 'unarchive') setArchived(id, a === 'archive');
|
|
1478
1517
|
if (a === 'delete') deleteSession(id);
|
|
@@ -1492,6 +1531,96 @@ async function deleteSession(id) {
|
|
|
1492
1531
|
await api(`/sessions/${id}`, { method: 'DELETE' }).catch((err) => toast(esc(err.message), { tone: 'bad' }));
|
|
1493
1532
|
}
|
|
1494
1533
|
|
|
1534
|
+
// Launch the task again as a new session in the same folder: same prompt and attachments,
|
|
1535
|
+
// optionally edited, on whichever harness, model and thinking level you pick. The original stays.
|
|
1536
|
+
async function rerun(id) {
|
|
1537
|
+
const s = S.full.get(id) || (await api(`/sessions/${id}`).catch((err) => toast(esc(err.message), { tone: 'bad' })));
|
|
1538
|
+
const first = s?.turns?.[0];
|
|
1539
|
+
if (!first?.prompt) return toast('Nothing to rerun — this session has no prompt.', { tone: 'bad' });
|
|
1540
|
+
const has = (a) => S.agents.some((x) => x.id === a && x.available);
|
|
1541
|
+
let agent = has(s.agent) ? s.agent : S.agents.find((x) => x.available)?.id || s.agent;
|
|
1542
|
+
const models = S.agents.find((x) => x.id === agent)?.models || [];
|
|
1543
|
+
let model = agent === s.agent && models.some((m) => m.id === s.model) ? s.model : savedModel(agent);
|
|
1544
|
+
let effort = agent === s.agent && effortsFor(agent, model).includes(s.effort) ? s.effort : savedEffort(agent, model);
|
|
1545
|
+
let mode = MODES.some((m) => m.id === s.mode) ? s.mode : S.config.defaults.mode || 'edits';
|
|
1546
|
+
const files = first.attachments || [];
|
|
1547
|
+
|
|
1548
|
+
const node = el(`
|
|
1549
|
+
<div class="modal rerun-modal">
|
|
1550
|
+
<header><h2>Rerun task</h2><button class="icon-btn close">${icon('x')}</button></header>
|
|
1551
|
+
<div class="rr-box">
|
|
1552
|
+
<textarea class="rr-prompt" rows="3" spellcheck="false"></textarea>
|
|
1553
|
+
${files.length ? `<div class="rr-files">${files.map((f) => `<span class="rr-file">${icon('clip')}${esc(f.name)}</span>`).join('')}</div>` : ''}
|
|
1554
|
+
<div class="l-bar">
|
|
1555
|
+
<button class="mode-btn" data-a="mode" aria-haspopup="menu"></button>
|
|
1556
|
+
<button class="runner-btn" data-a="runner" title="Harness, model and thinking level" aria-haspopup="dialog"></button>
|
|
1557
|
+
<span class="grow"></span>
|
|
1558
|
+
<button class="primary sm" data-a="go">${icon('rerun')}Rerun</button>
|
|
1559
|
+
</div>
|
|
1560
|
+
</div>
|
|
1561
|
+
<p class="hint">Starts a new session in <code>${esc(s.cwd)}</code>. “${esc(s.title)}” is kept as it is.</p>
|
|
1562
|
+
</div>`);
|
|
1563
|
+
const close = overlay(node);
|
|
1564
|
+
$('.close', node).onclick = close;
|
|
1565
|
+
const ta = $('.rr-prompt', node);
|
|
1566
|
+
ta.value = first.prompt;
|
|
1567
|
+
autosize(ta);
|
|
1568
|
+
const modeBtn = $('[data-a="mode"]', node);
|
|
1569
|
+
const paintMode = () => {
|
|
1570
|
+
const m = MODES.find((x) => x.id === mode) || MODES[1];
|
|
1571
|
+
modeBtn.className = `mode-btn m-${m.id}`;
|
|
1572
|
+
modeBtn.innerHTML = `${icon(m.icon)}<span>${m.label}</span>`;
|
|
1573
|
+
modeBtn.title = m.hint;
|
|
1574
|
+
};
|
|
1575
|
+
const runnerBtn = $('[data-a="runner"]', node);
|
|
1576
|
+
const paintRunner = () => {
|
|
1577
|
+
const a = S.agents.find((x) => x.id === agent);
|
|
1578
|
+
const m = (a?.models || []).find((x) => x.id === model);
|
|
1579
|
+
runnerBtn.innerHTML = `<span class="rb-logo">${harnessLogo(agent)}</span><span class="rb-h">${esc(a?.name || agentName(agent))}</span><span class="rb-m">${esc(m && m.id ? m.label : 'Default')}</span>${effort ? `<span class="rb-e">${esc(effortLabel(effort))}</span>` : ''}${icon('chevron', 'chev')}`;
|
|
1580
|
+
};
|
|
1581
|
+
paintMode();
|
|
1582
|
+
paintRunner();
|
|
1583
|
+
modeBtn.onclick = () => modeMenu(modeBtn, mode, (m) => ((mode = m), paintMode(), ta.focus()));
|
|
1584
|
+
// Picks here only apply to this rerun; the launcher's remembered defaults are left alone.
|
|
1585
|
+
runnerBtn.onclick = () =>
|
|
1586
|
+
runnerPicker(runnerBtn, { agent, model, effort, savedModel, savedEffort }, (a, m, e) => {
|
|
1587
|
+
[agent, model, effort] = [a, m, e];
|
|
1588
|
+
paintRunner();
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
const go = async () => {
|
|
1592
|
+
const prompt = ta.value.trim();
|
|
1593
|
+
if (!prompt) return ta.focus();
|
|
1594
|
+
const btn = $('[data-a="go"]', node);
|
|
1595
|
+
btn.disabled = true;
|
|
1596
|
+
try {
|
|
1597
|
+
const next = await api('/sessions', { method: 'POST', body: { prompt, cwd: s.cwd, agent, mode, model, effort, attachments: files.map((f) => f.id) } });
|
|
1598
|
+
S.sessions.set(next.id, next);
|
|
1599
|
+
close();
|
|
1600
|
+
if (S.filter !== 'all' && S.filter !== next.projectId) {
|
|
1601
|
+
S.filter = 'all';
|
|
1602
|
+
save('wisp.filter', S.filter);
|
|
1603
|
+
}
|
|
1604
|
+
renderFeed();
|
|
1605
|
+
setFocus(next.id);
|
|
1606
|
+
toast(`Rerunning on ${esc(agentName(agent))}`);
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
btn.disabled = false;
|
|
1609
|
+
toast(esc(err.message), { tone: 'bad' });
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
$('[data-a="go"]', node).onclick = go;
|
|
1613
|
+
ta.addEventListener('keydown', (e) => {
|
|
1614
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) (e.preventDefault(), go());
|
|
1615
|
+
if (e.key === 'Tab' && e.shiftKey) {
|
|
1616
|
+
e.preventDefault();
|
|
1617
|
+
mode = MODES[(MODES.findIndex((m) => m.id === mode) + 1) % MODES.length].id;
|
|
1618
|
+
paintMode();
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
requestAnimationFrame(() => (ta.focus(), ta.setSelectionRange(ta.value.length, ta.value.length)));
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1495
1624
|
async function setArchived(id, archived) {
|
|
1496
1625
|
try {
|
|
1497
1626
|
await api(`/sessions/${id}`, { method: 'PATCH', body: { archived } });
|
|
@@ -1702,6 +1831,7 @@ function openSettings() {
|
|
|
1702
1831
|
<input name="terminalCommand" value="${esc(d.terminalCommand || '')}" placeholder="alacritty --working-directory {cwd} -e {cmd}" spellcheck="false"></label>
|
|
1703
1832
|
<div class="setting"><span>Desktop notifications</span><span class="notif-hint muted xs"></span><button class="switch" id="notif" role="switch"></button></div>
|
|
1704
1833
|
<div class="setting"><span>Anonymous usage stats</span><span class="tele-hint muted xs"></span><button class="switch" id="tele" role="switch"></button></div>
|
|
1834
|
+
<div class="setting"><span>Memory</span><span class="learn-hint muted xs"></span><button class="ghost xs" id="openMemory">Open</button><button class="switch" id="learn" role="switch"></button></div>
|
|
1705
1835
|
<div class="agents">${S.agents
|
|
1706
1836
|
.map((a) => `<div class="agent-row"><span class="dot ${a.available ? 'ok' : 'bad'}"></span><b>${esc(a.name)}</b><span class="muted">${esc(a.available ? a.version : a.error || 'not found')}</span></div>`)
|
|
1707
1837
|
.join('')}<p class="muted xs">Pick the agent next to the model in the launcher. Others plug in via <code>server/adapters</code>.</p></div>
|
|
@@ -1748,6 +1878,160 @@ function openSettings() {
|
|
|
1748
1878
|
paintTele();
|
|
1749
1879
|
};
|
|
1750
1880
|
paintTele();
|
|
1881
|
+
// Memory: one line here (on/off and a summary); the lists and retros live in their own view.
|
|
1882
|
+
const learn = $('#learn', node);
|
|
1883
|
+
const paintLearn = async () => {
|
|
1884
|
+
learn.setAttribute('aria-checked', S.config.defaults.learn !== 'off');
|
|
1885
|
+
$('.learn-hint', node).textContent = memorySummary(await api('/preferences').catch(() => null));
|
|
1886
|
+
};
|
|
1887
|
+
learn.onclick = async () => {
|
|
1888
|
+
await setLearning(S.config.defaults.learn === 'off');
|
|
1889
|
+
paintLearn();
|
|
1890
|
+
};
|
|
1891
|
+
$('#openMemory', node).onclick = () => openMemory();
|
|
1892
|
+
node.paintLearn = paintLearn;
|
|
1893
|
+
paintLearn();
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// --- memory: what the background retros learned ---------------------------------------------
|
|
1897
|
+
|
|
1898
|
+
const MEM_KINDS = [
|
|
1899
|
+
{ kind: 'preferences', label: 'Preferences', one: 'preference', icon: 'spark', hint: 'How you like agents to work', empty: 'Corrections and “always / never” asks from your sessions land here.' },
|
|
1900
|
+
{ kind: 'lessons', label: 'Lessons', one: 'lesson', icon: 'check', hint: 'A mistake an agent made, and the way that worked', empty: 'When an agent hits a wall and finds the fix, the fix lands here.' },
|
|
1901
|
+
];
|
|
1902
|
+
const MEM_CAP = 10;
|
|
1903
|
+
const memCount = (x) => x.preferences.length + x.lessons.length;
|
|
1904
|
+
const projectName = (p) => S.config.projects.find((x) => x.path === p)?.name || String(p || '').split(/[\\/]/).pop();
|
|
1905
|
+
|
|
1906
|
+
async function setLearning(on) {
|
|
1907
|
+
await api('/config', { method: 'PATCH', body: { defaults: { learn: on ? 'on' : 'off' } } });
|
|
1908
|
+
await loadConfig();
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function memorySummary(data) {
|
|
1912
|
+
if (!data) return '';
|
|
1913
|
+
if (!data.enabled) return 'Off · nothing learned or applied';
|
|
1914
|
+
const n = memCount(data.global) + data.projects.reduce((a, p) => a + memCount(p), 0);
|
|
1915
|
+
const last = data.history[0];
|
|
1916
|
+
if (data.running) return `Retro running · ${n} remembered`;
|
|
1917
|
+
return `${n ? `${n} remembered` : 'Nothing yet'}${last ? ` · last retro ${ago(last.at)}` : ''}`;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function openMemory({ tab = 'global' } = {}) {
|
|
1921
|
+
const node = el(`<div class="modal memory-modal">
|
|
1922
|
+
<header><h2>Memory</h2><span class="mem-live"></span><button class="switch" id="memOn" role="switch" title="Learn from sessions"></button><button class="icon-btn close">${icon('x')}</button></header>
|
|
1923
|
+
<p class="mem-lede">Learned from your sessions and added to every agent's prompt. What you ask always wins.</p>
|
|
1924
|
+
<div class="mem-tabs" role="tablist"></div>
|
|
1925
|
+
<div class="mem-body"></div>
|
|
1926
|
+
</div>`);
|
|
1927
|
+
const close = overlay(node);
|
|
1928
|
+
$('.close', node).onclick = close;
|
|
1929
|
+
let data;
|
|
1930
|
+
let editing = null; // { scope, kind, index } (index -1 = adding)
|
|
1931
|
+
|
|
1932
|
+
const render = async () => {
|
|
1933
|
+
data = (await api('/preferences').catch(() => null)) || data;
|
|
1934
|
+
if (!data || !node.isConnected) return;
|
|
1935
|
+
// Projects with something learned, plus the one picked in the launcher so you can add to it.
|
|
1936
|
+
const current = projectById(launchProject);
|
|
1937
|
+
const projects = data.projects.filter((p) => memCount(p) || p.path === current?.path);
|
|
1938
|
+
if (current && !projects.some((p) => p.path === current.path)) projects.push({ path: current.path, name: current.name, preferences: [], lessons: [] });
|
|
1939
|
+
const scopes = [{ scope: 'global', name: 'Everywhere', ...data.global }, ...projects.map((p) => ({ scope: p.path, ...p }))];
|
|
1940
|
+
if (tab !== 'retros' && !scopes.some((x) => x.scope === tab)) tab = 'global';
|
|
1941
|
+
$('#memOn', node).setAttribute('aria-checked', data.enabled);
|
|
1942
|
+
$('.mem-live', node).innerHTML = data.running ? `<span class="orb live"></span>Retro running` : data.enabled ? '' : 'Off';
|
|
1943
|
+
$('.mem-tabs', node).innerHTML =
|
|
1944
|
+
scopes.map((x) => `<button role="tab" data-tab="${esc(x.scope)}" class="${tab === x.scope ? 'on' : ''}" title="${esc(x.scope === 'global' ? 'Applies in every project' : x.scope.replace(S.config.home, '~'))}">${esc(x.name)}<span>${memCount(x)}</span></button>`).join('') +
|
|
1945
|
+
`<i></i><button role="tab" data-tab="retros" class="${tab === 'retros' ? 'on' : ''}">Retros<span>${data.history.length}</span></button>`;
|
|
1946
|
+
const body = $('.mem-body', node);
|
|
1947
|
+
body.classList.toggle('off', !data.enabled);
|
|
1948
|
+
const banner = data.enabled ? '' : `<div class="mem-off">${icon('info')}<span>Memory is off: no retros run, and nothing below reaches your agents.</span><button class="ghost xs" data-act="on">Turn on</button></div>`;
|
|
1949
|
+
body.innerHTML = banner + (tab === 'retros' ? retrosView() : scopeView(scopes.find((x) => x.scope === tab)));
|
|
1950
|
+
const input = $('.mem-edit input', body);
|
|
1951
|
+
if (input) input.focus(), input.setSelectionRange(input.value.length, input.value.length);
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
// Which retro added an item, if it's still in the log.
|
|
1955
|
+
const origin = (scope, kind, text) => data.history.find((h) => h.added?.some((a) => a.text === text && a.kind === kind && (a.scope === 'global') === (scope === 'global')));
|
|
1956
|
+
|
|
1957
|
+
const scopeView = (x) => {
|
|
1958
|
+
const chars = [...(x.scope === 'global' ? [] : [...data.global.preferences, ...data.global.lessons]), ...x.preferences, ...x.lessons].reduce((a, t) => a + t.length + 3, 0);
|
|
1959
|
+
const budget = x.scope === 'global' ? `Added to every prompt · about ${Math.ceil(chars / 4)} tokens` : `Added to prompts in ${esc(x.name)}, with Everywhere · about ${Math.ceil(chars / 4)} tokens`;
|
|
1960
|
+
return `<p class="mem-budget muted xs">${budget}</p>` + MEM_KINDS.map((k) => {
|
|
1961
|
+
const items = x[k.kind];
|
|
1962
|
+
const row = (t, i) => {
|
|
1963
|
+
if (editing && editing.scope === x.scope && editing.kind === k.kind && editing.index === i) return editRow(t, k);
|
|
1964
|
+
const o = origin(x.scope, k.kind, t);
|
|
1965
|
+
return `<div class="mem-item" data-i="${i}"><span class="mem-ico">${icon(k.icon)}</span><div class="mem-main"><p>${esc(t)}</p>${o ? `<span class="mem-src">${o.sessionId && S.sessions.has(o.sessionId) ? `<a href="#" data-session="${esc(o.sessionId)}">${esc(o.title || 'session')}</a>` : esc(o.title || 'a session')} · ${ago(o.at)}</span>` : ''}</div>
|
|
1966
|
+
<span class="mem-acts"><button class="icon-btn" data-act="edit" title="Edit">${icon('pen')}</button><button class="icon-btn" data-act="forget" title="Forget">${icon('trash')}</button></span></div>`;
|
|
1967
|
+
};
|
|
1968
|
+
const adding = editing && editing.scope === x.scope && editing.kind === k.kind && editing.index === -1;
|
|
1969
|
+
return `<section class="mem-sec" data-scope="${esc(x.scope)}" data-kind="${k.kind}">
|
|
1970
|
+
<h4>${k.label}<span class="muted xs">${k.hint}</span><em class="${items.length >= MEM_CAP ? 'full' : ''}" title="At most ${MEM_CAP}; a retro drops the least useful when full">${items.length}/${MEM_CAP}</em></h4>
|
|
1971
|
+
${items.map(row).join('') || (adding ? '' : `<p class="mem-empty">${k.empty}</p>`)}
|
|
1972
|
+
${adding ? editRow('', k) : `<button class="mem-add" data-act="add">${icon('plus')}Add a ${k.one}</button>`}
|
|
1973
|
+
</section>`;
|
|
1974
|
+
}).join('');
|
|
1975
|
+
};
|
|
1976
|
+
const editRow = (t, k) => `<div class="mem-item mem-edit"><span class="mem-ico">${icon(k.icon)}</span><input value="${esc(t)}" maxlength="180" spellcheck="false" placeholder="One short sentence, e.g. “Run tests with node --test”"><span class="muted xs">↵ save · esc cancel</span></div>`;
|
|
1977
|
+
|
|
1978
|
+
const retrosView = () => {
|
|
1979
|
+
const live = data.running ? `<div class="tl running"><span class="tl-dot"><span class="orb live"></span></span><div class="tl-main"><div class="tl-head"><b>${esc(data.running.title || 'session')}</b><span class="muted xs">now</span></div><p class="muted xs">Reading ${data.running.turns} turn${data.running.turns === 1 ? '' : 's'} with ${esc(agentName(data.running.agent))}…</p></div></div>` : '';
|
|
1980
|
+
const where = (a) => `<span class="tl-tag">${a.scope === 'global' ? 'everywhere' : 'project'} · ${a.kind === 'lessons' ? 'lesson' : 'preference'}</span>`;
|
|
1981
|
+
const entries = data.history.map((h) => {
|
|
1982
|
+
const status = h.status || 'learned';
|
|
1983
|
+
const lines =
|
|
1984
|
+
status === 'failed' ? `<p class="tl-err">${esc(h.error || 'The retro run failed.')}</p>`
|
|
1985
|
+
: status === 'nothing' ? '<p class="muted xs">Nothing new worth remembering.</p>'
|
|
1986
|
+
: [...h.added.map((a) => `<p class="tl-add">${esc(a.text ?? a)} ${a.kind ? where(a) : ''}</p>`), ...h.removed.map((a) => `<p class="tl-del"><s>${esc(a.text ?? a)}</s> ${a.kind ? where(a) : ''}</p>`)].join('');
|
|
1987
|
+
const title = S.sessions.has(h.sessionId) ? `<a href="#" data-session="${esc(h.sessionId)}">${esc(h.title || 'session')}</a>` : esc(h.title || 'session');
|
|
1988
|
+
return `<div class="tl ${status}"><span class="tl-dot"></span><div class="tl-main"><div class="tl-head"><b>${title}</b>${h.project ? `<span class="muted xs">${esc(projectName(h.project))}</span>` : ''}<span class="muted xs">${ago(h.at)}</span></div>${lines}</div></div>`;
|
|
1989
|
+
});
|
|
1990
|
+
return live + entries.join('') || `<p class="mem-empty">No retros yet. One runs 90 s after a session finishes, or as soon as you mark it done or archive it.</p>`;
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
const save = async (scope, kind, items) => {
|
|
1994
|
+
await api('/preferences', { method: 'PUT', body: { scope, kind, items } }).catch((err) => toast(esc(err.message), { tone: 'bad' }));
|
|
1995
|
+
editing = null;
|
|
1996
|
+
await render();
|
|
1997
|
+
};
|
|
1998
|
+
const commit = (input) => {
|
|
1999
|
+
if (!editing) return;
|
|
2000
|
+
const { scope, kind, index } = editing;
|
|
2001
|
+
editing = null; // Enter then blur must not save twice
|
|
2002
|
+
const list = ((scope === 'global' ? data.global : data.projects.find((p) => p.path === scope)) || { [kind]: [] })[kind].slice();
|
|
2003
|
+
const v = input.value.trim();
|
|
2004
|
+
if (index === -1) v ? list.push(v) : null;
|
|
2005
|
+
else v ? (list[index] = v) : list.splice(index, 1);
|
|
2006
|
+
save(scope, kind, list);
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
node.addEventListener('click', async (e) => {
|
|
2010
|
+
const t = e.target.closest('[data-tab],[data-act],[data-session]');
|
|
2011
|
+
if (!t) return;
|
|
2012
|
+
e.preventDefault();
|
|
2013
|
+
if (t.dataset.tab) return (tab = t.dataset.tab), (editing = null), render();
|
|
2014
|
+
if (t.dataset.session) return close(), reveal(t.dataset.session);
|
|
2015
|
+
const act = t.dataset.act;
|
|
2016
|
+
if (act === 'on') return setLearning(true).then(render);
|
|
2017
|
+
const sec = t.closest('.mem-sec');
|
|
2018
|
+
const { scope, kind } = sec.dataset;
|
|
2019
|
+
const index = Number(t.closest('.mem-item')?.dataset.i ?? -1);
|
|
2020
|
+
if (act === 'add' || act === 'edit') return (editing = { scope, kind, index }), render();
|
|
2021
|
+
if (act === 'forget') {
|
|
2022
|
+
const list = (scope === 'global' ? data.global : data.projects.find((p) => p.path === scope))[kind].filter((_, i) => i !== index);
|
|
2023
|
+
return save(scope, kind, list);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
node.addEventListener('keydown', (e) => {
|
|
2027
|
+
if (!e.target.matches('.mem-edit input')) return;
|
|
2028
|
+
if (e.key === 'Enter') e.preventDefault(), commit(e.target);
|
|
2029
|
+
if (e.key === 'Escape') e.preventDefault(), e.stopPropagation(), (editing = null), render();
|
|
2030
|
+
});
|
|
2031
|
+
node.addEventListener('focusout', (e) => e.target.matches('.mem-edit input') && editing && commit(e.target));
|
|
2032
|
+
$('#memOn', node).onclick = async () => (await setLearning(!data?.enabled), render());
|
|
2033
|
+
node.refresh = () => !editing && render();
|
|
2034
|
+
render();
|
|
1751
2035
|
}
|
|
1752
2036
|
|
|
1753
2037
|
// Say once that anonymous stats are on, with a way out.
|
|
@@ -1771,6 +2055,7 @@ function shortcuts() {
|
|
|
1771
2055
|
['⌫', 'Delete item'],
|
|
1772
2056
|
['T', 'Take over in terminal'],
|
|
1773
2057
|
['R', 'Replay presentation'],
|
|
2058
|
+
['⇧R', 'Rerun with another harness, model or thinking level'],
|
|
1774
2059
|
['1–9', 'Pick an answer when asked'],
|
|
1775
2060
|
['↵', 'Launch / send'],
|
|
1776
2061
|
['⇧↵', 'New line'],
|
|
@@ -1817,6 +2102,7 @@ function paletteItems() {
|
|
|
1817
2102
|
if (!live) items.push({ group: g, label: cur.state === 'done' ? 'Move back to review' : 'Mark done', sub, icon: 'done', kbd: 'E', run: () => toggleDone(id) });
|
|
1818
2103
|
if (cur.archived || !live) items.push({ group: g, label: cur.archived ? 'Unarchive' : 'Archive', sub, icon: 'archive', kbd: 'A', run: () => setArchived(id, !cur.archived) });
|
|
1819
2104
|
items.push({ group: g, label: 'Take over in terminal', sub, icon: 'terminal', kbd: 'T', run: () => takeOver(id) });
|
|
2105
|
+
items.push({ group: g, label: 'Rerun…', sub, icon: 'rerun', kbd: '⇧R', keywords: 'retry again restart model harness', run: () => rerun(id) });
|
|
1820
2106
|
items.push({ group: g, label: 'Replay presentation', sub, icon: 'replay', kbd: 'R', run: () => withCard(id, () => replay(id), { open: true }) });
|
|
1821
2107
|
items.push({ group: g, label: 'Rename', sub, icon: 'pen', kbd: 'M', run: () => withCard(id, () => rename(id)) });
|
|
1822
2108
|
items.push({ group: g, label: 'Delete session', sub, icon: 'trash', kbd: '⌫', keywords: 'remove', run: () => deleteSession(id) });
|
|
@@ -1847,7 +2133,8 @@ function paletteItems() {
|
|
|
1847
2133
|
items.push(
|
|
1848
2134
|
{ group: a, label: 'Add project…', icon: 'folder', keywords: 'folder repo directory', run: () => openProjects({ focusAdd: true }) },
|
|
1849
2135
|
{ group: a, label: 'Manage projects', icon: 'folder', keywords: 'rename color remove', run: () => openProjects() },
|
|
1850
|
-
{ group: a, label: '
|
|
2136
|
+
{ group: a, label: 'Memory', icon: 'spark', keywords: 'learned preferences lessons retro', run: () => openMemory() },
|
|
2137
|
+
{ group: a, label: 'Settings', icon: 'settings', keywords: 'preferences lessons learn retro editor terminal notifications', run: openSettings },
|
|
1851
2138
|
{ group: a, label: 'Keyboard shortcuts', icon: 'info', kbd: '?', keywords: 'keys help hotkeys', run: shortcuts },
|
|
1852
2139
|
);
|
|
1853
2140
|
|
|
@@ -1909,7 +2196,13 @@ function connect() {
|
|
|
1909
2196
|
// fold it unless the user is actually watching it.
|
|
1910
2197
|
const n = S.nodes.get(s.id);
|
|
1911
2198
|
if (prev?.state === 'running' && s.state !== 'running' && S.open.has(s.id) && !(n && isInView(n.card) && document.hasFocus())) collapse(s.id);
|
|
2199
|
+
// A state change moves the card to another section (a follow-up sends it from
|
|
2200
|
+
// "Needs you" to "Working"). If the user is looking at it, follow it there
|
|
2201
|
+
// instead of leaving them on whatever slid into its place. (Marking done
|
|
2202
|
+
// moves focus to the next item on purpose, so that case is left alone.)
|
|
2203
|
+
const pin = prev && prev.state !== s.state && s.state !== 'done' && n?.card.isConnected && isInView(n.card) ? n.card.getBoundingClientRect().top : null;
|
|
1912
2204
|
renderFeed();
|
|
2205
|
+
if (pin !== null && n.card.isConnected) stage.scrollTop += n.card.getBoundingClientRect().top - pin;
|
|
1913
2206
|
if (prev?.state !== s.state || prev?.queued !== s.queued) scheduleRefetch(s.id);
|
|
1914
2207
|
});
|
|
1915
2208
|
es.addEventListener('activity', (e) => onActivity(JSON.parse(e.data)));
|
|
@@ -1924,7 +2217,12 @@ function connect() {
|
|
|
1924
2217
|
renderFeed();
|
|
1925
2218
|
});
|
|
1926
2219
|
es.addEventListener('config', () => loadConfig());
|
|
1927
|
-
es.addEventListener('
|
|
2220
|
+
es.addEventListener('update', (e) => renderUpdate(JSON.parse(e.data)));
|
|
2221
|
+
es.addEventListener('preferences', () => {
|
|
2222
|
+
$('.memory-modal')?.refresh();
|
|
2223
|
+
$('.settings-modal')?.paintLearn?.();
|
|
2224
|
+
});
|
|
2225
|
+
es.addEventListener('hello', () => (loadSessions(), loadUpdate())); // also after sleep/reconnect
|
|
1928
2226
|
}
|
|
1929
2227
|
|
|
1930
2228
|
async function loadSessions() {
|
|
@@ -1942,6 +2240,48 @@ async function loadConfig() {
|
|
|
1942
2240
|
renderLaunchChips();
|
|
1943
2241
|
}
|
|
1944
2242
|
|
|
2243
|
+
// --- updates ----------------------------------------------------------------------------
|
|
2244
|
+
|
|
2245
|
+
// The server polls npm; when a newer wisp-agents is out, a pill appears next to Search.
|
|
2246
|
+
let update = null;
|
|
2247
|
+
const loadUpdate = () => api('/update').then(renderUpdate).catch(() => {});
|
|
2248
|
+
function renderUpdate(u) {
|
|
2249
|
+
update = u;
|
|
2250
|
+
const b = $('#updateBtn');
|
|
2251
|
+
const busy = u.status === 'installing' || u.status === 'restarting';
|
|
2252
|
+
b.hidden = !u.available && !busy;
|
|
2253
|
+
b.disabled = busy;
|
|
2254
|
+
b.classList.toggle('busy', busy);
|
|
2255
|
+
b.querySelector('span').textContent = u.status === 'installing' ? 'Updating…' : u.status === 'restarting' ? 'Restarting…' : `Update to ${u.latest}`;
|
|
2256
|
+
b.title = u.auto ? `wisp ${u.current} → ${u.latest}: install and restart` : `wisp ${u.latest} is out. Run \`${u.command}\` to update`;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
async function installUpdate() {
|
|
2260
|
+
if (!update?.available) return;
|
|
2261
|
+
if (!update.auto) return toast(`wisp <b>${esc(update.latest)}</b> is out. Run <code>${esc(update.command)}</code>`, { action: { label: 'Copy', run: () => copy(update.command) }, timeout: 8000 });
|
|
2262
|
+
const post = (force) => api('/update', { method: 'POST', body: { force } });
|
|
2263
|
+
try {
|
|
2264
|
+
await post(false).catch(async (err) => {
|
|
2265
|
+
const running = [...S.sessions.values()].filter((s) => s.state === 'running' || s.state === 'waiting').length;
|
|
2266
|
+
if (err.message !== 'agents are running') throw err;
|
|
2267
|
+
if (!confirm(`Update to wisp ${update.latest} and restart? ${running || 'Some'} running agent${running === 1 ? '' : 's'} will be stopped.`)) throw null;
|
|
2268
|
+
return post(true);
|
|
2269
|
+
});
|
|
2270
|
+
} catch (err) {
|
|
2271
|
+
if (err) toast(esc(err.message), { tone: 'bad', timeout: 10_000, action: { label: 'Copy command', run: () => copy(update.command) } });
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
renderUpdate({ ...update, status: 'restarting' });
|
|
2275
|
+
// The server exits and a new one takes the port: reload once it answers with the new version.
|
|
2276
|
+
const from = update.current;
|
|
2277
|
+
const t0 = Date.now();
|
|
2278
|
+
const poll = setInterval(async () => {
|
|
2279
|
+
const h = await fetch('/api/health').then((r) => r.json()).catch(() => null);
|
|
2280
|
+
if (h?.version && h.version !== from) return clearInterval(poll), location.reload();
|
|
2281
|
+
if (Date.now() - t0 > 120_000) clearInterval(poll), toast('wisp did not come back. Start it again from your terminal.', { tone: 'bad', timeout: 15_000 });
|
|
2282
|
+
}, 1000);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1945
2285
|
// --- keyboard ---------------------------------------------------------------------------
|
|
1946
2286
|
|
|
1947
2287
|
const feedIds = () => $$('#feed .item').map((x) => x.dataset.id);
|
|
@@ -1962,7 +2302,10 @@ document.addEventListener('keydown', (e) => {
|
|
|
1962
2302
|
if (e.key === 'Escape') {
|
|
1963
2303
|
if (!$('#overlay').hidden) return closeOverlay();
|
|
1964
2304
|
if (typing) return e.target.blur();
|
|
1965
|
-
if (S.focus && S.open.has(S.focus))
|
|
2305
|
+
if (S.focus && S.open.has(S.focus)) {
|
|
2306
|
+
collapse(S.focus);
|
|
2307
|
+
S.nodes.get(S.focus)?.card.scrollIntoView({ block: 'nearest' });
|
|
2308
|
+
}
|
|
1966
2309
|
return;
|
|
1967
2310
|
}
|
|
1968
2311
|
if (typing || e.metaKey || e.ctrlKey || e.altKey || !$('#overlay').hidden) return;
|
|
@@ -1993,6 +2336,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
1993
2336
|
e.preventDefault();
|
|
1994
2337
|
deleteSession(id);
|
|
1995
2338
|
} else if (k === 't' && id) takeOver(id);
|
|
2339
|
+
else if (k === 'r' && e.shiftKey && id) rerun(id);
|
|
1996
2340
|
else if (k === 'r' && id) replay(id);
|
|
1997
2341
|
else if (k === '/' && id) {
|
|
1998
2342
|
e.preventDefault();
|
|
@@ -2009,6 +2353,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
2009
2353
|
|
|
2010
2354
|
$('#settingsBtn').onclick = openSettings;
|
|
2011
2355
|
$('#searchBtn').onclick = openPalette;
|
|
2356
|
+
$('#updateBtn').onclick = installUpdate;
|
|
2012
2357
|
$('#searchBtn').title = `Search (${combo('K')})`;
|
|
2013
2358
|
$('#searchBtn .mod-k').textContent = combo('K');
|
|
2014
2359
|
$('#secDone .toggle-done').onclick = () => {
|
|
@@ -2055,6 +2400,7 @@ function route() {
|
|
|
2055
2400
|
|
|
2056
2401
|
window.addEventListener('hashchange', route);
|
|
2057
2402
|
window.addEventListener('focus', () => {
|
|
2403
|
+
loadUpdate();
|
|
2058
2404
|
for (const id of S.open) if (S.sessions.get(id)?.unread) scheduleRefetch(id);
|
|
2059
2405
|
});
|
|
2060
2406
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Antigravity</title><mask height="23" id="lobe-icons-antigravity-0-_R_0_" maskUnits="userSpaceOnUse" width="24" x="0" y="1"><path d="M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z" fill="#fff"></path></mask><g mask="url(#lobe-icons-antigravity-0-_R_0_)"><g filter="url(#lobe-icons-antigravity-1-_R_0_)"><path d="M-1.018-3.992c-.408 3.591 2.686 6.89 6.91 7.37 4.225.48 7.98-2.043 8.387-5.633.408-3.59-2.686-6.89-6.91-7.37-4.225-.479-7.98 2.043-8.387 5.633z" fill="#FFE432"></path></g><g filter="url(#lobe-icons-antigravity-2-_R_0_)"><path d="M15.269 7.747c1.058 4.557 5.691 7.374 10.348 6.293 4.657-1.082 7.575-5.653 6.516-10.21-1.058-4.556-5.691-7.374-10.348-6.292-4.657 1.082-7.575 5.653-6.516 10.21z" fill="#FC413D"></path></g><g filter="url(#lobe-icons-antigravity-3-_R_0_)"><path d="M-12.443 10.804c1.338 4.703 7.36 7.11 13.453 5.378 6.092-1.733 9.947-6.95 8.61-11.652C8.282-.173 2.26-2.58-3.833-.848-9.925.884-13.78 6.1-12.443 10.804z" fill="#00B95C"></path></g><g filter="url(#lobe-icons-antigravity-4-_R_0_)"><path d="M-12.443 10.804c1.338 4.703 7.36 7.11 13.453 5.378 6.092-1.733 9.947-6.95 8.61-11.652C8.282-.173 2.26-2.58-3.833-.848-9.925.884-13.78 6.1-12.443 10.804z" fill="#00B95C"></path></g><g filter="url(#lobe-icons-antigravity-5-_R_0_)"><path d="M-7.608 14.703c3.352 3.424 9.126 3.208 12.896-.483 3.77-3.69 4.108-9.459.756-12.883C2.69-2.087-3.083-1.871-6.853 1.82c-3.77 3.69-4.108 9.458-.755 12.883z" fill="#00B95C"></path></g><g filter="url(#lobe-icons-antigravity-6-_R_0_)"><path d="M9.932 27.617c1.04 4.482 5.384 7.303 9.7 6.3 4.316-1.002 6.971-5.448 5.93-9.93-1.04-4.483-5.384-7.304-9.7-6.301-4.316 1.002-6.971 5.448-5.93 9.93z" fill="#3186FF"></path></g><g filter="url(#lobe-icons-antigravity-7-_R_0_)"><path d="M2.572-8.185C.392-3.329 2.778 2.472 7.9 4.771c5.122 2.3 11.042.227 13.222-4.63 2.18-4.855-.205-10.656-5.327-12.955-5.122-2.3-11.042-.227-13.222 4.63z" fill="#FBBC04"></path></g><g filter="url(#lobe-icons-antigravity-8-_R_0_)"><path d="M-3.267 38.686c-5.277-2.072 3.742-19.117 5.984-24.83 2.243-5.712 8.34-8.664 13.616-6.592 5.278 2.071 11.533 13.482 9.29 19.195-2.242 5.713-23.613 14.298-28.89 12.227z" fill="#3186FF"></path></g><g filter="url(#lobe-icons-antigravity-9-_R_0_)"><path d="M28.71 17.471c-1.413 1.649-5.1.808-8.236-1.878-3.135-2.687-4.531-6.201-3.118-7.85 1.412-1.649 5.1-.808 8.235 1.878s4.532 6.2 3.119 7.85z" fill="#749BFF"></path></g><g filter="url(#lobe-icons-antigravity-10-_R_0_)"><path d="M18.163 9.077c5.81 3.93 12.502 4.19 14.946.577 2.443-3.612-.287-9.727-6.098-13.658-5.81-3.931-12.502-4.19-14.946-.577-2.443 3.612.287 9.727 6.098 13.658z" fill="#FC413D"></path></g><g filter="url(#lobe-icons-antigravity-11-_R_0_)"><path d="M-.915 2.684c-1.44 3.473-.97 6.967 1.05 7.804 2.02.837 4.824-1.3 6.264-4.772 1.44-3.473.97-6.967-1.05-7.804-2.02-.837-4.824 1.3-6.264 4.772z" fill="#FFEE48"></path></g></g><defs><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="17.587" id="lobe-icons-antigravity-1-_R_0_" width="19.838" x="-3.288" y="-11.917"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="1.117"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="38.565" id="lobe-icons-antigravity-2-_R_0_" width="38.9" x="4.251" y="-13.493"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="5.4"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="36.517" id="lobe-icons-antigravity-3-_R_0_" width="40.955" x="-21.889" y="-10.592"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="4.591"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="36.517" id="lobe-icons-antigravity-4-_R_0_" width="40.955" x="-21.889" y="-10.592"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="4.591"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="36.595" id="lobe-icons-antigravity-5-_R_0_" width="36.632" x="-19.099" y="-10.278"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="4.591"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="34.087" id="lobe-icons-antigravity-6-_R_0_" width="33.533" x=".981" y="8.758"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="4.363"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="35.276" id="lobe-icons-antigravity-7-_R_0_" width="35.978" x="-6.143" y="-21.659"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="3.954"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="46.523" id="lobe-icons-antigravity-8-_R_0_" width="45.114" x="-11.96" y="-.46"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="3.531"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="24.054" id="lobe-icons-antigravity-9-_R_0_" width="25.094" x="10.485" y=".58"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="3.159"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="30.007" id="lobe-icons-antigravity-10-_R_0_" width="33.508" x="5.833" y="-12.467"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="2.669"></feGaussianBlur></filter><filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="26.151" id="lobe-icons-antigravity-11-_R_0_" width="22.194" x="-8.355" y="-8.876"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"></feBlend><feGaussianBlur result="effect1_foregroundBlur_977_115" stdDeviation="3.303"></feGaussianBlur></filter></defs></svg>
|