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/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
- const collapseAll = () => [...S.open].forEach((id) => collapse(id));
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 = () => preview(b.dataset.id)));
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: 'Settings', icon: 'settings', keywords: 'preferences editor terminal notifications', run: openSettings },
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('hello', () => loadSessions());
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)) collapse(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>