wisp-agents 0.17.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 +72 -1
- package/README.md +20 -7
- package/bin/wisp.js +4 -2
- 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/git.js +114 -2
- package/server/index.js +2 -1
- package/server/judge.js +78 -0
- package/server/machines.js +2 -2
- package/server/openapi.js +19 -2
- 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/meta.js +1 -1
- package/server/routes/settings.js +53 -4
- package/server/routes/ship.js +29 -2
- package/server/runner.js +65 -15
- package/server/scout.js +71 -0
- package/server/services/auto.js +115 -15
- package/server/services/launch.js +22 -13
- package/server/services/ship.js +107 -6
- package/server/store.js +15 -0
- package/web/css/auto.css +22 -0
- package/web/css/launcher.css +17 -0
- package/web/css/layout.css +10 -2
- package/web/css/modals.css +8 -0
- package/web/css/palette.css +22 -0
- package/web/css/ship.css +44 -0
- package/web/index.html +16 -5
- package/web/js/app.js +14 -3
- package/web/js/blocks.js +1 -1
- package/web/js/core/state.js +2 -1
- package/web/js/features/actions.js +2 -4
- package/web/js/features/auto.js +38 -7
- package/web/js/features/clones.js +79 -0
- package/web/js/features/composer.js +2 -4
- package/web/js/features/feed.js +34 -4
- package/web/js/features/launcher.js +139 -19
- package/web/js/features/live.js +5 -10
- package/web/js/features/project-list.js +99 -0
- package/web/js/features/projects.js +220 -39
- package/web/js/features/runners.js +7 -6
- package/web/js/features/settings.js +47 -7
- package/web/js/features/setup.js +3 -3
- package/web/js/features/ship.js +128 -25
- package/web/js/features/turns.js +4 -3
- package/web/js/ui/popover.js +10 -0
package/web/js/features/ship.js
CHANGED
|
@@ -7,26 +7,42 @@ import { S } from '../core/state.js';
|
|
|
7
7
|
import { overlay } from '../ui/overlay.js';
|
|
8
8
|
import { toast, toastError } from '../ui/toast.js';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// fix: the session's conflictFix ({ step, why }) while wisp resolves the PR's merge conflicts itself.
|
|
11
|
+
const fixing = (fix) => fix?.step === 'fixing' || fix?.step === 'pushing';
|
|
12
|
+
const prTone = (pr, fix) =>
|
|
13
|
+
pr.state === 'MERGED' ? 'merged' : pr.state === 'CLOSED' ? 'closed' : pr.conflict ? (fixing(fix) ? 'pending' : 'fail') : pr.checks?.fail ? 'fail' : pr.checks?.pending ? 'pending' : pr.review === 'CHANGES_REQUESTED' ? 'fail' : 'pass';
|
|
12
14
|
const REVIEW = { APPROVED: 'Approved', CHANGES_REQUESTED: 'Changes requested', REVIEW_REQUIRED: 'Review required' };
|
|
13
|
-
function prLine(pr) {
|
|
15
|
+
function prLine(pr, fix) {
|
|
16
|
+
if (pr.state === 'OPEN' && (pr.conflict || fixing(fix))) {
|
|
17
|
+
const what = fix?.step === 'pushing' ? 'Pushing the conflict fix' : fixing(fix) ? 'Fixing merge conflicts' : fix?.step === 'failed' ? `Conflicts not fixed: ${fix.why}` : 'Merge conflicts';
|
|
18
|
+
return `Open · ${what}`;
|
|
19
|
+
}
|
|
14
20
|
const c = pr.checks || {};
|
|
15
21
|
const checks = c.fail ? `${c.fail} check${c.fail > 1 ? 's' : ''} failing` : c.pending ? `${c.pending} check${c.pending > 1 ? 's' : ''} running` : c.pass ? `${c.pass} check${c.pass > 1 ? 's' : ''} passed` : 'No checks';
|
|
16
22
|
const state = pr.state === 'MERGED' ? 'Merged' : pr.state === 'CLOSED' ? 'Closed' : pr.draft ? 'Draft' : 'Open';
|
|
17
23
|
return [state, pr.state === 'OPEN' ? checks : '', pr.state === 'OPEN' ? REVIEW[pr.review] : ''].filter(Boolean).join(' · ');
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
// A ship in flight, per session: { steps, step, update, route, error, values }. The dialog closes
|
|
27
|
+
// on confirm and the card follows along here (the server sends the live stage as `shipping`);
|
|
28
|
+
// a failure stays on the card with Retry, which reopens the dialog with what was typed.
|
|
29
|
+
const ships = new Map();
|
|
30
|
+
const STEP_LABEL = { commit: 'Commit', push: 'Push', pr: 'Open PR' };
|
|
31
|
+
const STEP_DOING = { commit: 'Committing', push: 'Pushing', pr: 'Opening the PR' };
|
|
32
|
+
const shipping = (s) => !!(s?.shipping || (ships.has(s?.id) && !ships.get(s.id).error));
|
|
33
|
+
|
|
20
34
|
// Ship it only makes sense on a finished worktree task with something to ship: uncommitted
|
|
21
35
|
// changes, or commits its open PR (or a new one) doesn't have yet. Unchecked yet: assume it might.
|
|
22
36
|
export function shippable(s) {
|
|
23
|
-
if (!s?.branch || s.agent === 'wisp' || s.state === 'running' || s.state === 'waiting' || s.pr?.state === 'MERGED') return false;
|
|
37
|
+
if (!s?.branch || s.agent === 'wisp' || s.state === 'running' || s.state === 'waiting' || s.pr?.state === 'MERGED' || shipping(s)) return false;
|
|
24
38
|
const t = s.toShip;
|
|
25
39
|
return !t || !!(t.dirty || (s.pr?.state === 'OPEN' ? t.unpushed : t.ahead));
|
|
26
40
|
}
|
|
27
41
|
export const shipLabel = (s) => (s.pr?.state === 'OPEN' ? `Push to PR #${s.pr.number}` : 'Ship it…');
|
|
28
42
|
|
|
29
|
-
export const
|
|
43
|
+
export const shipPill = (s) => (s.shipping ? `<span class="pill ship-pill" title="Shipping: ${esc(STEP_DOING[s.shipping.step] || 'Starting')}…"><i></i>Shipping</span>` : '');
|
|
44
|
+
|
|
45
|
+
export const prPill = (pr, fix) => `<span class="pill pr pr-${prTone(pr, fix)}" title="PR #${pr.number}: ${esc(prLine(pr, fix))}"><i></i>#${pr.number}</span>`;
|
|
30
46
|
|
|
31
47
|
// The PR row under a card's bar; reads the live summary, so polled check results show up.
|
|
32
48
|
export function renderPrStrip(id) {
|
|
@@ -39,25 +55,92 @@ export function renderPrStrip(id) {
|
|
|
39
55
|
b.hidden = !shippable(s);
|
|
40
56
|
$('span', b).textContent = pr?.state === 'OPEN' ? `Push to #${pr.number}` : pr ? 'Ship again' : 'Ship it';
|
|
41
57
|
});
|
|
58
|
+
// Started here, or elsewhere (another tab, the API): the server's stage moves the track along.
|
|
59
|
+
const run = ships.get(id) || (s.shipping && { steps: s.shipping.update ? ['push', 'pr'] : ['commit', 'push', 'pr'], step: 'push' });
|
|
60
|
+
if (run && STEP_LABEL[s.shipping?.step] && !run.error) run.step = s.shipping.step;
|
|
61
|
+
if (run) return renderShipRun(node, id, run);
|
|
62
|
+
const landed = node.classList.contains('ship-run') && !node.classList.contains('failed');
|
|
42
63
|
node.hidden = !pr;
|
|
43
64
|
if (!pr) return;
|
|
44
|
-
const tone = prTone(pr);
|
|
45
|
-
node.className = `pr-strip pr-${tone}`;
|
|
65
|
+
const tone = prTone(pr, s.conflictFix);
|
|
66
|
+
node.className = `pr-strip pr-${tone}${landed ? ' landed' : ''}`;
|
|
46
67
|
node.innerHTML = `
|
|
47
68
|
<span class="pr-dot"></span>
|
|
48
69
|
<a href="${esc(pr.url)}" target="_blank" class="pr-num">PR #${pr.number}</a>
|
|
49
|
-
<span class="pr-line">${esc(prLine(pr))}</span>
|
|
70
|
+
<span class="pr-line">${esc(prLine(pr, s.conflictFix))}</span>
|
|
50
71
|
<span class="grow"></span>
|
|
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>` : ''}
|
|
51
73
|
${pr.state === 'OPEN' ? `<button class="ghost xs" data-pr="refresh" title="Check now (wisp also checks every minute)">${icon('replay')}</button>` : ''}
|
|
52
74
|
<a class="ghost xs" href="${esc(pr.url)}" target="_blank">${icon('external')}<span>GitHub</span></a>
|
|
53
|
-
${pr.state === 'OPEN' ? `<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>` : ''}`;
|
|
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>` : ''}`;
|
|
54
76
|
node.onclick = (e) => {
|
|
55
77
|
const a = e.target.closest('[data-pr]')?.dataset.pr;
|
|
56
|
-
if (a === 'refresh')
|
|
78
|
+
if (a === 'refresh') refreshPr(id, e.target.closest('[data-pr]'));
|
|
57
79
|
if (a === 'merge') mergePr(id);
|
|
80
|
+
if (a === 'fix') fixPr(id);
|
|
58
81
|
};
|
|
59
82
|
}
|
|
60
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
|
+
|
|
111
|
+
// The in-flight row: the stages as a track that fills in as the server reports them. Built once
|
|
112
|
+
// and updated in place, so its entry animation and shimmer don't restart with every summary.
|
|
113
|
+
function renderShipRun(node, id, run) {
|
|
114
|
+
const at = Math.max(run.steps.indexOf(run.step), 0);
|
|
115
|
+
if (!node.classList.contains('ship-run') || node.dataset.steps !== run.steps.join()) {
|
|
116
|
+
node.dataset.steps = run.steps.join();
|
|
117
|
+
node.innerHTML = `
|
|
118
|
+
<span class="ship-icon">${icon('send')}</span>
|
|
119
|
+
<span class="ship-what"></span>
|
|
120
|
+
<ol class="ship-track">${run.steps.map((k) => `<li data-step="${k}"><i>${icon('check')}</i><span>${STEP_LABEL[k]}</span></li>`).join('')}</ol>
|
|
121
|
+
<span class="grow"></span>
|
|
122
|
+
<span class="ship-acts"></span>
|
|
123
|
+
<span class="ship-bar"></span>`;
|
|
124
|
+
node.onclick = (e) => {
|
|
125
|
+
const a = e.target.closest('[data-ship]')?.dataset.ship;
|
|
126
|
+
if (a === 'retry') shipDialog(id);
|
|
127
|
+
if (a === 'dismiss') {
|
|
128
|
+
ships.delete(id);
|
|
129
|
+
renderPrStrip(id);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const failed = !!run.error;
|
|
134
|
+
node.hidden = false;
|
|
135
|
+
node.className = `pr-strip ship-run${failed ? ' failed pr-fail' : ''}`;
|
|
136
|
+
node.title = failed ? run.error : '';
|
|
137
|
+
$$('.ship-track li', node).forEach((li, i) => (li.className = i < at ? 'done' : i === at ? (failed ? 'bad' : 'now') : ''));
|
|
138
|
+
const what = failed ? `Ship failed at ${STEP_LABEL[run.steps[at]] || 'start'}: <span class="ship-err">${esc(run.error)}</span>` : `${STEP_DOING[run.steps[at]]}<span class="dots"></span>`;
|
|
139
|
+
if ($('.ship-what', node).innerHTML !== what) $('.ship-what', node).innerHTML = what;
|
|
140
|
+
const acts = failed ? `<button class="ghost xs" data-ship="retry">${icon('replay')}<span>Retry</span></button><button class="ghost xs" data-ship="dismiss" title="Dismiss">${icon('x')}</button>` : '';
|
|
141
|
+
if ($('.ship-acts', node).innerHTML !== acts) $('.ship-acts', node).innerHTML = acts;
|
|
142
|
+
}
|
|
143
|
+
|
|
61
144
|
export async function shipDialog(id) {
|
|
62
145
|
const s = S.sessions.get(id);
|
|
63
146
|
if (!s) return;
|
|
@@ -69,6 +152,7 @@ export async function shipDialog(id) {
|
|
|
69
152
|
return toastError(err);
|
|
70
153
|
}
|
|
71
154
|
const open = info.pr?.state === 'OPEN';
|
|
155
|
+
const typed = ships.get(id)?.values; // a failed ship's message, back for Retry
|
|
72
156
|
const nothing = !info.dirty && !info.ahead;
|
|
73
157
|
const node = el(`
|
|
74
158
|
<div class="modal ship-modal">
|
|
@@ -85,23 +169,42 @@ export async function shipDialog(id) {
|
|
|
85
169
|
$('.close', node).onclick = close;
|
|
86
170
|
const title = $('[name="title"]', node);
|
|
87
171
|
const body = $('[name="body"]', node);
|
|
88
|
-
if (title) title.value = info.title;
|
|
89
|
-
if (body) body.value = info.body;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
close();
|
|
97
|
-
toast(`${icon('branch')} ${open ? 'Pushed to' : 'Opened'} <b>PR #${pr.number}</b>`, { action: { label: 'Open', run: () => window.open(pr.url, '_blank') }, timeout: 8000 });
|
|
98
|
-
} catch (err) {
|
|
99
|
-
btn.disabled = false;
|
|
100
|
-
btn.lastChild.textContent = open ? 'Commit & push' : 'Commit, push & open PR';
|
|
101
|
-
toastError(err, { timeout: 10_000 });
|
|
172
|
+
if (title) title.value = typed?.title || info.title;
|
|
173
|
+
if (body) body.value = typed?.body ?? info.body;
|
|
174
|
+
if (typed?.draft && $('[name="draft"]', node)) $('[name="draft"]', node).checked = true;
|
|
175
|
+
// Confirm closes the dialog at once; the card's PR row shows the stages, then the PR itself.
|
|
176
|
+
const go = () => {
|
|
177
|
+
if (title && !title.value.trim()) {
|
|
178
|
+
title.focus();
|
|
179
|
+
return toast('A commit message is required.', { tone: 'bad' });
|
|
102
180
|
}
|
|
181
|
+
const values = { title: title?.value || info.title, body: body?.value ?? info.body, draft: !!$('[name="draft"]', node)?.checked };
|
|
182
|
+
close();
|
|
183
|
+
ships.set(id, { steps: [...(info.dirty ? ['commit'] : []), 'push', 'pr'], step: info.dirty ? 'commit' : 'push', update: open, values });
|
|
184
|
+
renderPrStrip(id);
|
|
185
|
+
const strip = S.nodes.get(id)?.body?.querySelector('.pr-strip');
|
|
186
|
+
if (strip && !strip.hidden) strip.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
187
|
+
api(`/sessions/${id}/ship`, { method: 'POST', body: values }).then(
|
|
188
|
+
(pr) => {
|
|
189
|
+
const s = S.sessions.get(id);
|
|
190
|
+
// Everything just went up: no Push to #n until the server's recheck finds something new.
|
|
191
|
+
if (s) S.sessions.set(id, { ...s, pr: { ...s.pr, ...pr }, shipping: null, toShip: s.toShip && { ...s.toShip, dirty: 0, unpushed: 0 } });
|
|
192
|
+
ships.delete(id);
|
|
193
|
+
renderPrStrip(id);
|
|
194
|
+
toast(`${icon('branch')} ${open ? 'Pushed to' : 'Opened'} <b>PR #${pr.number}</b>`, { action: { label: 'Open', run: () => window.open(pr.url, '_blank') }, timeout: 8000 });
|
|
195
|
+
},
|
|
196
|
+
(err) => {
|
|
197
|
+
if (ships.has(id)) ships.get(id).error = err.message;
|
|
198
|
+
const s = S.sessions.get(id);
|
|
199
|
+
if (s) S.sessions.set(id, { ...s, shipping: null });
|
|
200
|
+
renderPrStrip(id);
|
|
201
|
+
toastError(err, { timeout: 10_000 });
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
$('[data-a="go"]', node).onclick = () => {
|
|
206
|
+
go();
|
|
103
207
|
};
|
|
104
|
-
$('[data-a="go"]', node).onclick = go;
|
|
105
208
|
requestAnimationFrame(() => (title || $('[data-a="go"]', node)).focus());
|
|
106
209
|
}
|
|
107
210
|
|
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
|
+
}
|