wisp-agents 0.18.0 → 0.20.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 +51 -1
- package/README.md +19 -7
- package/package.json +2 -2
- package/server/adapters/antigravity.js +2 -1
- package/server/adapters/claude.js +2 -1
- package/server/adapters/codex.js +2 -1
- package/server/adapters/commands.js +808 -0
- package/server/adapters/cursor.js +2 -1
- package/server/adapters/grok.js +2 -1
- package/server/adapters/opencode.js +2 -1
- package/server/check.js +205 -0
- package/server/index.js +2 -1
- package/server/judge.js +78 -0
- package/server/openapi.js +3 -3
- package/server/pipeline.js +217 -0
- package/server/quick.js +76 -0
- package/server/router.js +201 -28
- package/server/routes/auto.js +8 -1
- package/server/routes/settings.js +2 -0
- package/server/routes/ship.js +9 -4
- package/server/runner.js +59 -13
- package/server/scout.js +71 -0
- package/server/services/auto.js +103 -14
- package/server/services/launch.js +11 -6
- package/server/services/ship.js +36 -14
- package/web/css/auto.css +22 -0
- package/web/css/ship.css +5 -1
- package/web/index.html +1 -0
- package/web/js/blocks.js +1 -1
- package/web/js/features/actions.js +2 -4
- package/web/js/features/auto.js +38 -7
- package/web/js/features/composer.js +2 -4
- package/web/js/features/feed.js +31 -1
- package/web/js/features/live.js +2 -7
- package/web/js/features/projects.js +2 -5
- package/web/js/features/runners.js +7 -6
- package/web/js/features/settings.js +34 -8
- package/web/js/features/ship.js +30 -3
- package/web/js/features/turns.js +4 -3
- package/web/js/ui/popover.js +10 -0
package/web/js/features/auto.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
// Auto: what the router (server/router.js) learned about which harness and
|
|
1
|
+
// Auto: what the router (server/router.js) learned about which harness, model and thinking level work for you.
|
|
2
2
|
|
|
3
3
|
import { esc } from '../md.js';
|
|
4
4
|
import { api } from '../core/api.js';
|
|
5
5
|
import * as bus from '../core/bus.js';
|
|
6
|
-
import { $, el, icon } from '../core/dom.js';
|
|
6
|
+
import { $, $$, el, icon } from '../core/dom.js';
|
|
7
7
|
import { S } from '../core/state.js';
|
|
8
8
|
import { present } from '../ui/overlay.js';
|
|
9
|
-
import { toastError } from '../ui/toast.js';
|
|
9
|
+
import { toast, toastError } from '../ui/toast.js';
|
|
10
10
|
import { AUTO_GOALS, harnessLogo } from './runners.js';
|
|
11
11
|
|
|
12
|
-
const ARM_STATE = { ready: ['Ready', 'ok'], new: ['Trying out', 'warn'], untried: ['No data yet', ''], gone: ['Not installed', 'bad'] };
|
|
12
|
+
const ARM_STATE = { ready: ['Ready', 'ok'], new: ['Trying out', 'warn'], untried: ['No data yet', ''], cooling: ['At its limit', 'warn'], gone: ['Not installed', 'bad'] };
|
|
13
|
+
const TIER = { 1: 'small', 2: 'everyday', 3: 'frontier' };
|
|
14
|
+
const untilLabel = (t) => new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
13
15
|
export async function openRouter({ host } = {}) {
|
|
14
16
|
const node = el(`<div class="modal router-modal">
|
|
15
17
|
<header><h2>Auto</h2><button class="icon-btn close">${icon('x')}</button></header>
|
|
@@ -23,15 +25,22 @@ export async function openRouter({ host } = {}) {
|
|
|
23
25
|
if (!r || !node.isConnected) return;
|
|
24
26
|
const arms = r.arms.filter((a) => a.state !== 'gone' || a.n);
|
|
25
27
|
$('.ro-body', node).innerHTML = `
|
|
26
|
-
<p class="hint">For each task Auto estimates every harness · model’s chance of doing it well, from wisp-bench (${esc(r.priors.version)}) and then more and more from your own tasks: merged PRs, 👍 / 👎, rollbacks, follow-ups and how the run ended. It picks the best value for your goal (<b>${esc(AUTO_GOALS.find((g) => g.id === r.goal)?.label || '')}</b>),
|
|
27
|
-
<h4>Harness · model</h4>
|
|
28
|
+
<p class="hint">For each task Auto estimates every harness · model · thinking level’s chance of doing it well, from wisp-bench (${esc(r.priors.version)}) and then more and more from your own tasks: merged PRs, 👍 / 👎, rollbacks, follow-ups and how the run ended. It picks the best value for your goal (<b>${esc(AUTO_GOALS.find((g) => g.id === r.goal)?.label || '')}</b>), which also sets how hard it thinks: Cheap low, Balanced default or medium, Best high. Best runs frontier models only (Opus, not Sonnet or Haiku). Now and then it tries an alternative so the estimates stay honest.${r.coldStart && r.coldStart.have < r.coldStart.needed ? ` Until it has ${r.coldStart.needed} of your own outcomes (${r.coldStart.have} so far), Balanced uses everyday and frontier models only.` : ''}</p>
|
|
29
|
+
<h4>Harness · model · thinking</h4>
|
|
28
30
|
<table class="us-table ro-table"><thead><tr><th></th><th>Works</th><th>Your tasks</th><th>~Cost</th><th>~Time</th><th></th></tr></thead><tbody>${arms
|
|
29
31
|
.map((a) => {
|
|
30
32
|
const [state, tone] = ARM_STATE[a.state] || ARM_STATE.ready;
|
|
31
33
|
const types = Object.entries(a.byType).map(([t, x]) => `${t} ${pctOf(x.chance)} (${x.n})`).join(' · ');
|
|
32
|
-
return `<tr><td><span class="us-runner"><span class="model-logo">${harnessLogo(a.agent)}</span>${esc(a.label)}</span>${types ? `<small class="ro-types">${esc(types)}</small>` : ''}</td>${a.state === 'untried' ? '<td>—</td><td>—</td><td>—</td><td>—</td>' : `<td><b>${pctOf(a.chance)}</b></td><td>${a.n || '—'}</td><td>${a.usd ? `$${a.usd.toFixed(2)}` : '$0'}</td><td>${a.minutes}m</td>`}<td><span class="sx-state ${tone}"><i class="dot ${tone}"></i>${state}</span></td></tr>`;
|
|
34
|
+
return `<tr><td><span class="us-runner"><span class="model-logo">${harnessLogo(a.agent)}</span>${esc(a.label)}<i class="ro-tier t${a.tier}" title="${TIER[a.tier]} tier">${TIER[a.tier]}</i></span>${types ? `<small class="ro-types">${esc(types)}</small>` : ''}</td>${a.state === 'untried' ? '<td>—</td><td>—</td><td>—</td><td>—</td>' : `<td><b>${pctOf(a.chance)}</b></td><td>${a.n || '—'}</td><td>${a.usd ? `$${a.usd.toFixed(2)}` : '$0'}</td><td>${a.minutes}m</td>`}<td><span class="sx-state ${tone}" ${a.coolingUntil ? `title="${esc(`${a.coolingWhy || 'At its limit'} · back around ${untilLabel(a.coolingUntil)}`)}"` : ''}><i class="dot ${tone}"></i>${state}${a.coolingUntil ? ` until ${untilLabel(a.coolingUntil)}` : ''}</span></td></tr>`;
|
|
33
35
|
})
|
|
34
36
|
.join('')}</tbody></table>
|
|
37
|
+
<h4>Tests per project</h4>
|
|
38
|
+
<p class="hint">What wisp runs to check an Auto task. Leave it empty to use what wisp found, or type <code>off</code> to skip the check in that project.</p>
|
|
39
|
+
${r.projects?.length
|
|
40
|
+
? `<div class="ro-tests">${r.projects
|
|
41
|
+
.map((p) => `<label class="ro-test"><span class="us-proj c-${esc(p.color || 'slate')}"><i class="p-dot"></i>${esc(p.name)}</span><input class="input mono" data-id="${esc(p.id)}" value="${esc(p.testCommand)}" placeholder="${esc(p.detected?.command || (p.detected?.skip ? `none: ${p.detected.skip}` : 'none found'))}" spellcheck="false"></label>`)
|
|
42
|
+
.join('')}</div>`
|
|
43
|
+
: '<p class="mem-empty">Add a project to set its test command.</p>'}
|
|
35
44
|
<h4>Recent picks</h4>
|
|
36
45
|
${r.recent.length
|
|
37
46
|
? `<ul class="ro-recent">${r.recent
|
|
@@ -42,6 +51,28 @@ export async function openRouter({ host } = {}) {
|
|
|
42
51
|
.join('')}</ul>`
|
|
43
52
|
: '<p class="mem-empty">Nothing yet. Launch with Auto and its picks show up here.</p>'}
|
|
44
53
|
<div class="ro-foot"><button class="ghost sm danger" id="roReset">Forget what Auto learned</button><span class="hint">The wisp-bench starting point stays.</span></div>`;
|
|
54
|
+
for (const input of $$('.ro-test input', node)) {
|
|
55
|
+
let saved = input.value;
|
|
56
|
+
const save = async () => {
|
|
57
|
+
const v = input.value.trim();
|
|
58
|
+
if (v === saved) return;
|
|
59
|
+
const prev = saved;
|
|
60
|
+
saved = v;
|
|
61
|
+
try {
|
|
62
|
+
await api(`/projects/${input.dataset.id}`, { method: 'PATCH', body: { testCommand: v } });
|
|
63
|
+
toast(v ? `Auto will check with <code>${esc(v)}</code>` : 'Back to the test command wisp finds');
|
|
64
|
+
} catch (err) {
|
|
65
|
+
saved = input.value = prev;
|
|
66
|
+
toastError(err);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
input.onblur = () => {
|
|
70
|
+
save();
|
|
71
|
+
};
|
|
72
|
+
input.onkeydown = (e) => {
|
|
73
|
+
if (e.key === 'Enter') input.blur();
|
|
74
|
+
};
|
|
75
|
+
}
|
|
45
76
|
$('#roReset', node).onclick = async () => {
|
|
46
77
|
if (!confirm('Forget what Auto learned from your tasks?')) return;
|
|
47
78
|
await api('/router/reset', { method: 'POST', body: {} }).catch(toastError);
|
|
@@ -5,7 +5,7 @@ import { mentionHtml } from '../mention-text.js';
|
|
|
5
5
|
import { api } from '../core/api.js';
|
|
6
6
|
import { $, el, icon } from '../core/dom.js';
|
|
7
7
|
import { S } from '../core/state.js';
|
|
8
|
-
import { popover } from '../ui/popover.js';
|
|
8
|
+
import { clampToViewport, popover } from '../ui/popover.js';
|
|
9
9
|
import { toast, toastError } from '../ui/toast.js';
|
|
10
10
|
import { toggleDone } from './actions.js';
|
|
11
11
|
import { acceptFiles, attachTray, autosize } from './attachments.js';
|
|
@@ -226,9 +226,7 @@ function queuedMenu(e, cancel) {
|
|
|
226
226
|
if (a === 'cancel') cancel();
|
|
227
227
|
};
|
|
228
228
|
const close = popover(menu);
|
|
229
|
-
|
|
230
|
-
if (r.right > innerWidth - 8) menu.style.left = `${innerWidth - r.width - 8}px`;
|
|
231
|
-
if (r.bottom > innerHeight - 8) menu.style.top = `${innerHeight - r.height - 8}px`;
|
|
229
|
+
clampToViewport(menu, e.clientX, e.clientY);
|
|
232
230
|
}
|
|
233
231
|
|
|
234
232
|
// Follow-ups sent while an agent works interrupt it, unless the user flipped this on.
|
package/web/js/features/feed.js
CHANGED
|
@@ -208,6 +208,7 @@ function renderHead(n, s) {
|
|
|
208
208
|
<span class="it-side">
|
|
209
209
|
${pill}
|
|
210
210
|
${s.queued ? `<span class="q-queued" title="Follow-ups queued">+${s.queued}</span>` : ''}
|
|
211
|
+
${autoPill(s)}
|
|
211
212
|
${s.shipping ? shipPill(s) : s.pr ? prPill(s.pr, s.conflictFix) : ''}
|
|
212
213
|
${s.team?.length ? teamStack(s.agent, s.team) : modelBadge(s.agent, s.model, s.effort, s.route?.switched ? null : s.route)}
|
|
213
214
|
${s.machine ? machinePill(s.machine) : ''}
|
|
@@ -218,6 +219,28 @@ function renderHead(n, s) {
|
|
|
218
219
|
</span>`;
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
// What Auto found after the run: its own test check, the best-of-2 pick, a hand-over.
|
|
223
|
+
function autoPill(s) {
|
|
224
|
+
const a = s.auto;
|
|
225
|
+
if (!a || s.pending) return '';
|
|
226
|
+
const out = [];
|
|
227
|
+
if (a.pick?.winner) out.push(`<span class="pill auto-pick" title="${esc(`wisp's pick of the two runs: ${a.pick.why}`)}">${icon('spark')}Pick</span>`);
|
|
228
|
+
if (a.escalatedTo) {
|
|
229
|
+
const to = S.sessions.get(a.escalatedTo);
|
|
230
|
+
const toName = to ? `${agentName(to.agent)}${to.model ? ` · ${to.model}` : ''}` : 'another pick';
|
|
231
|
+
const why = a.reason === 'limit' ? `${agentName(s.agent)} was at its usage limit, so wisp moved the task to ${toName}` : a.reason === 'error' ? `The run failed, so wisp handed the task to ${toName}` : `Tests still failed, so wisp handed the task to ${toName}`;
|
|
232
|
+
out.push(`<span class="pill auto-handed" title="${esc(why)}">${a.reason === 'limit' ? 'Moved on' : 'Handed on'}</span>`);
|
|
233
|
+
}
|
|
234
|
+
const c = a.check;
|
|
235
|
+
if (c && !a.busy && c.verdict !== 'skip') {
|
|
236
|
+
const n = c.newFailures || c.failed;
|
|
237
|
+
const tip = c.verdict === 'pass' ? `wisp ran ${c.command}: no new failures${c.note ? ` (${c.note})` : ''}` : c.verdict === 'fail' ? `wisp ran ${c.command}: ${n} test${n === 1 ? '' : 's'} that passed before now fail${a.repairs ? `, after ${a.repairs} repair round${a.repairs > 1 ? 's' : ''}` : ''}` : `wisp ran ${c.command}: ${c.note || "couldn't tell"}`;
|
|
238
|
+
const label = c.verdict === 'pass' ? 'Tests' : c.verdict === 'fail' ? `${n} failing` : 'Tests ?';
|
|
239
|
+
out.push(`<span class="pill auto-ck ck-${c.verdict}" title="${esc(tip)}">${icon(c.verdict === 'pass' ? 'check' : c.verdict === 'fail' ? 'x' : 'info')}${label}</span>`);
|
|
240
|
+
}
|
|
241
|
+
return out.join('');
|
|
242
|
+
}
|
|
243
|
+
|
|
221
244
|
// One run inside a compare task: the harness and model stand in for the title the task shows.
|
|
222
245
|
function renderRunHead(n, s, { open, live, pill }) {
|
|
223
246
|
const a = S.agents.find((x) => x.id === s.agent);
|
|
@@ -233,6 +256,7 @@ function renderRunHead(n, s, { open, live, pill }) {
|
|
|
233
256
|
${runsOf(s).some((r) => (r.machine?.id || '') !== (s.machine?.id || '')) ? `${s.machine ? machinePill(s.machine) : `<span class="q-machine" title="Runs on this computer">${icon('laptop')}<span>This computer</span></span>`}<span class="q-proj">${esc(s.project)}</span>` : ''}
|
|
234
257
|
${live && s.state === 'running' ? `<span class="pill st-running"><i></i>Working</span>` : pill}
|
|
235
258
|
${s.queued ? `<span class="q-queued" title="Follow-ups queued">+${s.queued}</span>` : ''}
|
|
259
|
+
${autoPill(s)}
|
|
236
260
|
${s.shipping ? shipPill(s) : s.pr ? prPill(s.pr, s.conflictFix) : ''}
|
|
237
261
|
<span class="q-time">${ago(s.updatedAt)}</span>
|
|
238
262
|
${icon('chevron', 'chev')}
|
|
@@ -257,7 +281,13 @@ function taskItem(t) {
|
|
|
257
281
|
g.lead = t.id;
|
|
258
282
|
const finished = t.runs.filter((r) => r.state === 'review' || r.state === 'done').length;
|
|
259
283
|
const places = new Set(t.runs.map((r) => r.machine?.id || '')).size;
|
|
260
|
-
|
|
284
|
+
// Auto's groups say what Auto did: which run it picked, or who it handed the task to.
|
|
285
|
+
const picked = t.runs.find((r) => r.auto?.pick?.winner);
|
|
286
|
+
const handed = t.runs.find((r) => r.auto?.escalatedFrom);
|
|
287
|
+
const runName = (r) => `${agentName(r.agent)}${r.model ? ` · ${r.model}` : ''}`;
|
|
288
|
+
const how = handed?.auto?.reason === 'limit' ? 'The first harness was at its usage limit, so wisp moved it to' : handed?.auto?.reason === 'error' ? 'The first run failed, so wisp handed it to' : 'Tests failed, so wisp handed it to';
|
|
289
|
+
const autoLine = picked ? `wisp's pick: ${picked.auto.pick.why}` : handed ? `${how} ${runName(handed)}${handed.auto?.check?.verdict === 'pass' ? ' · tests pass now' : handed.auto?.check?.verdict === 'fail' ? ' · still failing' : ''}` : '';
|
|
290
|
+
const status = t.state === 'waiting' ? 'A run is asking you' : t.state === 'running' ? `${finished} of ${t.runs.length} finished${places > 1 ? ` · on ${places} machines` : ''}` : t.state === 'done' ? 'Done' : autoLine || (places > 1 ? `All finished on ${places} machines` : 'All finished · pick one to keep');
|
|
261
291
|
g.card.className = `task-item c-${t.color} st-${t.state} ${t.unread ? 'unread' : ''}`;
|
|
262
292
|
g.head.innerHTML = `
|
|
263
293
|
<span class="q-dot ${t.state === 'running' ? 'live' : ''} ${t.state === 'waiting' ? 'waiting' : ''}"></span>
|
package/web/js/features/live.js
CHANGED
|
@@ -7,7 +7,7 @@ import { $ } from '../core/dom.js';
|
|
|
7
7
|
import { S, saveOpen, setAgents } from '../core/state.js';
|
|
8
8
|
import { toast } from '../ui/toast.js';
|
|
9
9
|
import { paintAutoBtn } from './automations.js';
|
|
10
|
-
import {
|
|
10
|
+
import { renderFeed, scheduleRefetch, sectionOf } from './feed.js';
|
|
11
11
|
import { dropPending, renderLaunchChips } from './launcher.js';
|
|
12
12
|
import { loadMachines, setMachines } from './machines.js';
|
|
13
13
|
import { notifyFinished } from './notify.js';
|
|
@@ -34,12 +34,7 @@ export function connect() {
|
|
|
34
34
|
// list and focus goes to the next one instead.
|
|
35
35
|
const moved = prev && was !== sectionOf(s);
|
|
36
36
|
const follow = moved && s.state !== 'done' && !s.archived && watching(s.id);
|
|
37
|
-
keepInPlace(follow ? S.nodes.get(s.id).card : feedAnchor(s.id),
|
|
38
|
-
// A card left open while it worked shouldn't land in "Needs you" expanded —
|
|
39
|
-
// fold it unless the user is actually watching it.
|
|
40
|
-
if (prev?.state === 'running' && s.state !== 'running' && S.open.has(s.id) && !(watching(s.id) && document.hasFocus())) collapse(s.id);
|
|
41
|
-
renderFeed();
|
|
42
|
-
});
|
|
37
|
+
keepInPlace(follow ? S.nodes.get(s.id).card : feedAnchor(s.id), renderFeed);
|
|
43
38
|
if (prev?.state !== s.state || prev?.queued !== s.queued) scheduleRefetch(s.id);
|
|
44
39
|
if (JSON.stringify([prev?.pr, prev?.toShip, prev?.state, prev?.shipping]) !== JSON.stringify([s.pr, s.toShip, s.state, s.shipping])) renderPrStrip(s.id);
|
|
45
40
|
});
|
|
@@ -6,7 +6,7 @@ import * as bus from '../core/bus.js';
|
|
|
6
6
|
import { $, $$, el, icon } from '../core/dom.js';
|
|
7
7
|
import { launch, S, save, saveLaunch } from '../core/state.js';
|
|
8
8
|
import { present } from '../ui/overlay.js';
|
|
9
|
-
import { popover } from '../ui/popover.js';
|
|
9
|
+
import { clampToViewport, popover } from '../ui/popover.js';
|
|
10
10
|
import { toast, toastError, tryApi } from '../ui/toast.js';
|
|
11
11
|
import { clones, isRepo, repoLabel, repoName, startClone } from './clones.js';
|
|
12
12
|
import { feedToTop, renderFeed } from './feed.js';
|
|
@@ -146,10 +146,7 @@ function projectMenu(id, e) {
|
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
148
|
const close = popover(menu);
|
|
149
|
-
|
|
150
|
-
const r = menu.getBoundingClientRect();
|
|
151
|
-
if (r.right > innerWidth - 8) menu.style.left = `${innerWidth - r.width - 8}px`;
|
|
152
|
-
if (r.bottom > innerHeight - 8) menu.style.top = `${innerHeight - r.height - 8}px`;
|
|
149
|
+
clampToViewport(menu, e.clientX, e.clientY);
|
|
153
150
|
}
|
|
154
151
|
|
|
155
152
|
const COLORS = ['violet', 'teal', 'amber', 'rose', 'sky', 'lime', 'orange', 'indigo'];
|
|
@@ -17,14 +17,15 @@ export const DEFAULT_MODE = MODES[2];
|
|
|
17
17
|
|
|
18
18
|
export const agentName = (id) => S.agents.find((a) => a.id === id)?.name || (id === 'auto' ? 'Auto' : id === 'wisp' ? 'wisp' : id);
|
|
19
19
|
|
|
20
|
-
// Auto: not a CLI but wisp's router (server/router.js), which picks harness
|
|
21
|
-
// Its "models" are the goal it optimises for
|
|
20
|
+
// Auto: not a CLI but wisp's router (server/router.js), which picks harness, model and thinking
|
|
21
|
+
// level per task. Its "models" are the goal it optimises for, which also sets the level band, so
|
|
22
|
+
// there are no thinking levels to pick here.
|
|
22
23
|
export const AUTO_GOALS = [
|
|
23
|
-
{ id: 'balanced', label: 'Balanced', hint: 'Best result per dollar' },
|
|
24
|
-
{ id: 'cheap', label: 'Cheap', hint: 'Lowest cost that still gets it done' },
|
|
25
|
-
{ id: 'best', label: 'Best', hint: 'Best result; cost only breaks ties' },
|
|
24
|
+
{ id: 'balanced', label: 'Balanced', hint: 'Best result per dollar · default or medium thinking' },
|
|
25
|
+
{ id: 'cheap', label: 'Cheap', hint: 'Lowest cost that still gets it done · low thinking' },
|
|
26
|
+
{ id: 'best', label: 'Best', hint: 'Best result; cost only breaks ties · high thinking' },
|
|
26
27
|
];
|
|
27
|
-
export const AUTO = { id: 'auto', name: 'Auto', available: true, delegates: true, models: AUTO_GOALS, efforts: [], hint: 'wisp picks the harness and
|
|
28
|
+
export const AUTO = { id: 'auto', name: 'Auto', available: true, delegates: true, models: AUTO_GOALS, efforts: [], hint: 'wisp picks the harness, model and thinking level from what worked before' };
|
|
28
29
|
|
|
29
30
|
// Harness lists per place: '' is this computer, else a machine (what it reported when it came online).
|
|
30
31
|
export const agentsAt = (k) => (k ? (machineById(k)?.state === 'online' && machineById(k).remote?.agents) || null : S.agents);
|
|
@@ -78,7 +78,7 @@ export function openSettings(opts) {
|
|
|
78
78
|
</div>
|
|
79
79
|
<div class="sx-group">
|
|
80
80
|
${row('Worktree per task', 'Each task gets its own branch from origin/main, in git repos', '<button class="switch" id="wtDefault" role="switch"></button>')}
|
|
81
|
-
${row('
|
|
81
|
+
${row('Auto-fix PR conflicts', 'Without waiting for a click on Fix conflicts: when a shipped PR conflicts with its base, the task merges the base in, resolves it and pushes', '<button class="switch" id="fixConflicts" role="switch"></button>')}
|
|
82
82
|
</div>
|
|
83
83
|
</section>
|
|
84
84
|
<section class="sx-pane" data-pane="notifications">
|
|
@@ -101,6 +101,13 @@ export function openSettings(opts) {
|
|
|
101
101
|
${row('Try new models', 'A new model or harness gets a few tasks that aren’t marked important before Auto relies on it', '<button class="switch" id="roExplore" role="switch"></button>')}
|
|
102
102
|
${row('Harnesses it may use', 'Leave all off to let Auto use every one you have', '<span class="ro-allowed"></span>')}
|
|
103
103
|
</div>
|
|
104
|
+
<div class="sx-group">
|
|
105
|
+
${row('Check with the tests', 'When the agent says it’s done, wisp runs the project’s tests itself. Only failures that are new count', '<button class="switch" id="roCheck" role="switch"></button>')}
|
|
106
|
+
${row('Repair rounds', 'Failing tests go back to the same agent this many times', `<span class="pk-seg ro-repairs">${[0, 1, 2].map((n) => `<button data-n="${n}">${n}</button>`).join('')}</span>`)}
|
|
107
|
+
${row('Hand over when it still fails', 'The task runs again on a stronger pick, next to the first run', '<button class="switch" id="roEscalate" role="switch"></button>')}
|
|
108
|
+
${row('Best of two', 'Off by default. With the Best goal, two picks work side by side and wisp marks the better one', '<button class="switch" id="roBest2" role="switch"></button>')}
|
|
109
|
+
${row('Scout first', 'A cheap model looks around the repo and tells the agent where to start', `<span class="pk-seg ro-scout">${[['off', 'Off'], ['big', 'Bigger tasks'], ['all', 'Every task']].map(([v, l]) => `<button data-v="${v}">${l}</button>`).join('')}</span>`)}
|
|
110
|
+
</div>
|
|
104
111
|
<div class="sx-group">
|
|
105
112
|
${row('What it learned', '<span class="ro-hint"></span>', '<button class="ghost sm" id="openRouter">Open</button>')}
|
|
106
113
|
</div>
|
|
@@ -302,10 +309,10 @@ export function openSettings(opts) {
|
|
|
302
309
|
if (!saved) return;
|
|
303
310
|
paintWt();
|
|
304
311
|
const fc = $('#fixConflicts', node);
|
|
305
|
-
const paintFc = () => fc.setAttribute('aria-checked', S.config.defaults.fixConflicts
|
|
312
|
+
const paintFc = () => fc.setAttribute('aria-checked', S.config.defaults.fixConflicts === 'on');
|
|
306
313
|
fc.onclick = async () => {
|
|
307
314
|
const was = S.config.defaults.fixConflicts;
|
|
308
|
-
S.config.defaults.fixConflicts = was === '
|
|
315
|
+
S.config.defaults.fixConflicts = was === 'on' ? 'off' : 'on';
|
|
309
316
|
paintFc();
|
|
310
317
|
const saved = await tryApi(() => api('/config', { method: 'PATCH', body: { defaults: { fixConflicts: S.config.defaults.fixConflicts } } }));
|
|
311
318
|
if (!saved) {
|
|
@@ -323,13 +330,18 @@ export function openSettings(opts) {
|
|
|
323
330
|
if (!r || !node.isConnected) return;
|
|
324
331
|
$$('.ro-goal button', node).forEach((b) => b.classList.toggle('on', b.dataset.goal === r.goal));
|
|
325
332
|
$('#roExplore', node).setAttribute('aria-checked', r.settings.explore);
|
|
333
|
+
$('#roCheck', node).setAttribute('aria-checked', r.settings.check);
|
|
334
|
+
$('#roEscalate', node).setAttribute('aria-checked', r.settings.escalate);
|
|
335
|
+
$('#roBest2', node).setAttribute('aria-checked', r.settings.best2);
|
|
336
|
+
$$('.ro-repairs button', node).forEach((b) => b.classList.toggle('on', Number(b.dataset.n) === r.settings.repairs));
|
|
337
|
+
$$('.ro-scout button', node).forEach((b) => b.classList.toggle('on', b.dataset.v === r.settings.scout));
|
|
326
338
|
const allowed = r.settings.allowed || [];
|
|
327
339
|
$('.ro-allowed', node).innerHTML = S.agents
|
|
328
340
|
.filter((a) => a.available)
|
|
329
341
|
.map((a) => `<button class="chip-tog${allowed.includes(a.id) ? ' on' : ''}" data-id="${esc(a.id)}" aria-pressed="${allowed.includes(a.id)}" title="${esc(a.name)}"><span class="model-logo">${harnessLogo(a.id)}</span>${esc(a.name)}</button>`)
|
|
330
342
|
.join('');
|
|
331
343
|
$('.ro-hint', node).textContent = r.learned
|
|
332
|
-
? `${r.learned} task${r.learned === 1 ? '' : 's'} learned from · ${r.arms.filter((a) => a.state !== 'gone').length} harness · model
|
|
344
|
+
? `${r.learned} task${r.learned === 1 ? '' : 's'} learned from · ${r.arms.filter((a) => a.state !== 'gone').length} harness · model · level options`
|
|
333
345
|
: `Nothing of yours yet · starting from wisp-bench (${r.priors.version})`;
|
|
334
346
|
};
|
|
335
347
|
const patchAuto = async (body) => {
|
|
@@ -349,10 +361,24 @@ export function openSettings(opts) {
|
|
|
349
361
|
patchAuto({ goal: b.dataset.goal });
|
|
350
362
|
}
|
|
351
363
|
};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
364
|
+
for (const [id, key] of [['#roExplore', 'explore'], ['#roCheck', 'check'], ['#roEscalate', 'escalate'], ['#roBest2', 'best2']]) {
|
|
365
|
+
$(id, node).onclick = (e) => {
|
|
366
|
+
const on = e.currentTarget.getAttribute('aria-checked') !== 'true';
|
|
367
|
+
e.currentTarget.setAttribute('aria-checked', on);
|
|
368
|
+
patchAuto({ [key]: on });
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
$('.ro-repairs', node).onclick = (e) => {
|
|
372
|
+
const b = e.target.closest('[data-n]');
|
|
373
|
+
if (!b) return;
|
|
374
|
+
$$('.ro-repairs button', node).forEach((x) => x.classList.toggle('on', x === b));
|
|
375
|
+
patchAuto({ repairs: Number(b.dataset.n) });
|
|
376
|
+
};
|
|
377
|
+
$('.ro-scout', node).onclick = (e) => {
|
|
378
|
+
const b = e.target.closest('[data-v]');
|
|
379
|
+
if (!b) return;
|
|
380
|
+
$$('.ro-scout button', node).forEach((x) => x.classList.toggle('on', x === b));
|
|
381
|
+
patchAuto({ scout: b.dataset.v });
|
|
356
382
|
};
|
|
357
383
|
$('.ro-allowed', node).onclick = (e) => {
|
|
358
384
|
const b = e.target.closest('[data-id]');
|
package/web/js/features/ship.js
CHANGED
|
@@ -69,18 +69,45 @@ export function renderPrStrip(id) {
|
|
|
69
69
|
<a href="${esc(pr.url)}" target="_blank" class="pr-num">PR #${pr.number}</a>
|
|
70
70
|
<span class="pr-line">${esc(prLine(pr, s.conflictFix))}</span>
|
|
71
71
|
<span class="grow"></span>
|
|
72
|
-
${pr.state === 'OPEN' && pr.conflict && s.conflictFix
|
|
72
|
+
${pr.state === 'OPEN' && pr.conflict && !fixing(s.conflictFix) && s.state !== 'running' ? `<button class="ghost xs pr-fix" data-pr="fix" title="Send the task a follow-up to merge the base in and resolve the conflicts; wisp pushes the fix to the PR">${icon('merge')}<span>Fix conflicts</span></button>` : ''}
|
|
73
73
|
${pr.state === 'OPEN' ? `<button class="ghost xs" data-pr="refresh" title="Check now (wisp also checks every minute)">${icon('replay')}</button>` : ''}
|
|
74
74
|
<a class="ghost xs" href="${esc(pr.url)}" target="_blank">${icon('external')}<span>GitHub</span></a>
|
|
75
75
|
${pr.state === 'OPEN' && !pr.conflict ? `<button class="ghost xs pr-merge" data-pr="merge" title="Squash-merge, then fast-forward your local ${esc(s.branch ? 'base branch' : 'checkout')}">${icon('check')}<span>Merge</span></button>` : ''}`;
|
|
76
76
|
node.onclick = (e) => {
|
|
77
77
|
const a = e.target.closest('[data-pr]')?.dataset.pr;
|
|
78
|
-
if (a === 'refresh')
|
|
78
|
+
if (a === 'refresh') refreshPr(id, e.target.closest('[data-pr]'));
|
|
79
79
|
if (a === 'merge') mergePr(id);
|
|
80
|
-
if (a === 'fix')
|
|
80
|
+
if (a === 'fix') fixPr(id);
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Check now: the button spins at once and stops when the answer (pushed as a summary) is in.
|
|
85
|
+
function refreshPr(id, btn) {
|
|
86
|
+
btn.classList.add('busy');
|
|
87
|
+
btn.disabled = true;
|
|
88
|
+
api(`/sessions/${id}/pr`, { method: 'POST', body: {} })
|
|
89
|
+
.catch(toastError)
|
|
90
|
+
.finally(() => {
|
|
91
|
+
btn.classList.remove('busy');
|
|
92
|
+
btn.disabled = false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fix conflicts: the row flips to "Fixing merge conflicts" at once; a refusal puts it back.
|
|
97
|
+
function fixPr(id) {
|
|
98
|
+
const s = S.sessions.get(id);
|
|
99
|
+
if (!s) return;
|
|
100
|
+
const was = s.conflictFix;
|
|
101
|
+
s.conflictFix = { ...was, step: 'fixing', why: '' };
|
|
102
|
+
renderPrStrip(id);
|
|
103
|
+
api(`/sessions/${id}/fix-conflicts`, { method: 'POST', body: {} }).catch((err) => {
|
|
104
|
+
const now = S.sessions.get(id);
|
|
105
|
+
if (now?.conflictFix?.step === 'fixing') now.conflictFix = was;
|
|
106
|
+
renderPrStrip(id);
|
|
107
|
+
toastError(err);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
// The in-flight row: the stages as a track that fills in as the server reports them. Built once
|
|
85
112
|
// and updated in place, so its entry animation and shimmer don't restart with every summary.
|
|
86
113
|
function renderShipRun(node, id, run) {
|
package/web/js/features/turns.js
CHANGED
|
@@ -19,7 +19,7 @@ import { scrollByPx, watching } from './scroll.js';
|
|
|
19
19
|
import { renderPrStrip, shipDialog } from './ship.js';
|
|
20
20
|
import { copiesOf, memberLabel, renderSubs, subKey, teamLiveHtml, teamSize } from './team.js';
|
|
21
21
|
|
|
22
|
-
const KIND_ICON = { read: 'read', edit: 'pen', run: 'terminal', search: 'search', web: 'web', agent: 'agent', plan: 'plan', tool: 'tool', text: 'text', error: 'warn', ask: 'ask', answer: 'check', wait: 'ask', background: 'clock' };
|
|
22
|
+
const KIND_ICON = { check: 'check', read: 'read', edit: 'pen', run: 'terminal', search: 'search', web: 'web', agent: 'agent', plan: 'plan', tool: 'tool', text: 'text', error: 'warn', ask: 'ask', answer: 'check', wait: 'ask', background: 'clock' };
|
|
23
23
|
|
|
24
24
|
export function ctxFor(s, turn, isLatest) {
|
|
25
25
|
return {
|
|
@@ -169,7 +169,7 @@ function renderFoldedTurn(t, unfold) {
|
|
|
169
169
|
<article class="turn folded" data-turn="${t.id}">
|
|
170
170
|
<button class="t-fold" title="Show this exchange">
|
|
171
171
|
${icon('chevron', 'chev')}
|
|
172
|
-
<span class="t-prompt">${mentionHtml(esc(t.prompt.replace(/\s+/g, ' ')), { link: false })}</span>
|
|
172
|
+
<span class="t-prompt">${t.by === 'wisp' ? '<b class="t-by">wisp</b> ' : ''}${mentionHtml(esc(t.prompt.replace(/\s+/g, ' ')), { link: false })}</span>
|
|
173
173
|
${t.rolledBackAt ? '<span class="rev-tag">Rolled back</span>' : ''}
|
|
174
174
|
${hero?.title ? `<span class="t-result">${esc(hero.title)}</span>` : ''}
|
|
175
175
|
<time>${new Date(t.startedAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</time>
|
|
@@ -194,7 +194,8 @@ function renderTurn(s, t, isLatest, fold) {
|
|
|
194
194
|
const cp = t.checkpoint;
|
|
195
195
|
const cpLabel = cp?.head ? `${cp.branch ? `${cp.branch}@` : ''}${cp.head.slice(0, 7)}${cp.dirty ? '*' : ''}` : '';
|
|
196
196
|
const ask = el(`
|
|
197
|
-
<div class="ask">
|
|
197
|
+
<div class="ask${t.by === 'wisp' ? ' by-wisp' : ''}">
|
|
198
|
+
${t.by === 'wisp' ? `<div class="ask-by">${icon('spark')}Sent by wisp after its test run</div>` : ''}
|
|
198
199
|
<div class="ask-text ${long ? 'clamped' : ''}">${mentionHtml(esc(t.prompt))}</div>
|
|
199
200
|
${long ? '<button class="link xs more-btn">Show all</button>' : ''}
|
|
200
201
|
${t.attachments?.length ? `<div class="ask-atts">${t.attachments.map((a) => (a.mime?.startsWith('image/') ? `<img src="/api/uploads/${a.id}" alt="${esc(a.name)}" title="${esc(a.name)}">` : `<a class="att-pill" href="/api/uploads/${a.id}" target="_blank">${icon('file')}${esc(a.name)}</a>`)).join('')}</div>` : ''}
|
package/web/js/ui/popover.js
CHANGED
|
@@ -50,3 +50,13 @@ export function popover(node, { anchor, keep, capture = false, onClose } = {}) {
|
|
|
50
50
|
document.body.append(node);
|
|
51
51
|
return close;
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
// Keep a right-click menu at the pointer but on-screen near the window edges. Reads offset
|
|
55
|
+
// sizes: the menu's pop-in animation starts scaled down, so its bounding box is too small
|
|
56
|
+
// on the first frame and the last items would end up below the fold.
|
|
57
|
+
export function clampToViewport(node, x, y) {
|
|
58
|
+
const w = node.offsetWidth;
|
|
59
|
+
const h = node.offsetHeight;
|
|
60
|
+
node.style.left = `${Math.max(8, Math.min(x, innerWidth - w - 8))}px`;
|
|
61
|
+
node.style.top = `${Math.max(8, Math.min(y, innerHeight - h - 8))}px`;
|
|
62
|
+
}
|