wisp-agents 0.24.0 → 0.25.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 +23 -1
- package/README.md +44 -4
- package/bin/wisp.js +55 -1
- package/package.json +2 -2
- package/server/adapters/codex.js +3 -0
- package/server/client.js +2 -2
- package/server/index.js +17 -2
- package/server/mcp-ask.js +44 -2
- package/server/openapi.js +39 -1
- package/server/protocol.js +32 -3
- package/server/render.js +16 -1
- package/server/routes/automations.js +8 -5
- package/server/routes/internal.js +61 -0
- package/server/routes/meta.js +5 -1
- package/server/routes/settings.js +17 -3
- package/server/routes/spells.js +147 -0
- package/server/runner.js +10 -3
- package/server/services/launch.js +7 -2
- package/server/services/spell-hooks.js +99 -0
- package/server/spell-guide.js +135 -0
- package/server/spells-data.js +60 -0
- package/server/spells.js +647 -0
- package/server/update.js +50 -16
- package/web/css/feed.css +1 -1
- package/web/css/spells.css +205 -0
- package/web/index.html +4 -0
- package/web/js/app.js +3 -0
- package/web/js/blocks.js +7 -1
- package/web/js/features/actions.js +3 -0
- package/web/js/features/commands.js +11 -0
- package/web/js/features/composer.js +12 -0
- package/web/js/features/feed.js +3 -0
- package/web/js/features/keys.js +15 -0
- package/web/js/features/launcher.js +56 -11
- package/web/js/features/live.js +9 -1
- package/web/js/features/settings.js +21 -0
- package/web/js/features/slash.js +2 -1
- package/web/js/features/spell-actions.js +64 -0
- package/web/js/features/spell-ui.js +398 -0
- package/web/js/features/spells.js +715 -0
- package/web/js/features/turns.js +2 -0
- package/web/js/features/update.js +1 -2
- package/web/js/spell-schema.js +246 -0
|
@@ -27,6 +27,8 @@ import { needsSetup, nudgeSetup, renderSetup } from './setup.js';
|
|
|
27
27
|
import { agentName, agentsAt, DEFAULT_MODE, effortLabel, effortsFor, harnessesAt, harnessLogo, MAX_RUNNERS, modeMenu, MODES, paintRunnerBtn, runnerTip, savedEffort, savedModel } from './runners.js';
|
|
28
28
|
import { feedAnchor, keepInPlace, stage, topInset } from './scroll.js';
|
|
29
29
|
import { canLead, paintTeamBtn, teamEditor } from './team.js';
|
|
30
|
+
import { activeSpells, launcherParts, runAction, spellProp } from './spells.js';
|
|
31
|
+
import { cast } from './spell-ui.js';
|
|
30
32
|
|
|
31
33
|
// "No folder": a general ask with no project. The server gives it an empty scratch directory.
|
|
32
34
|
const NO_FOLDER = 'none';
|
|
@@ -72,6 +74,19 @@ function syncWhere() {
|
|
|
72
74
|
launch.project = storedPick() || null;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// A spell's slash command at the start of `text`: { spell, cmd, args } or null.
|
|
78
|
+
export function spellSlash(text) {
|
|
79
|
+
const m = /^\/([a-z][a-z0-9-]*)(?:\s+([\s\S]*))?$/i.exec(String(text || '').trim());
|
|
80
|
+
if (!m) return null;
|
|
81
|
+
for (const spell of activeSpells()) {
|
|
82
|
+
const cmd = (spell.slash || []).find((c) => c.name === m[1].toLowerCase());
|
|
83
|
+
if (cmd) return { spell, cmd, args: (m[2] || '').trim() };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
bus.on('spells:launcher', () => $('#launcher:not(.skel)') && renderLauncher());
|
|
89
|
+
|
|
75
90
|
export function renderLauncher() {
|
|
76
91
|
const box = $('#launcher');
|
|
77
92
|
const mode = S.config.defaults.mode || DEFAULT_MODE.id;
|
|
@@ -104,18 +119,19 @@ export function renderLauncher() {
|
|
|
104
119
|
<div class="l-recipes"></div>`;
|
|
105
120
|
renderLaunchChips();
|
|
106
121
|
renderSetup();
|
|
122
|
+
launcherParts(box); // what spells add to the prompt box
|
|
107
123
|
|
|
108
124
|
const ta = mentionBox($('#prompt', box));
|
|
109
125
|
const tray = attachTray();
|
|
110
126
|
ta.parentNode.before(tray.node); // before the mention box's wrapper
|
|
111
127
|
acceptFiles(box, ta, tray);
|
|
112
|
-
autosize(ta);
|
|
128
|
+
autosize(ta, (spellProp('launcher.input', 'maxRows') || 0) * 24 || undefined);
|
|
113
129
|
ta.value = S.drafts.get('home') || '';
|
|
114
130
|
ta.addEventListener('input', () => (S.drafts.set('home', ta.value), paintSaveRecipe()));
|
|
115
131
|
// While focus is elsewhere, the placeholder advertises the shortcut that brings you back.
|
|
116
|
-
const hint = () => (ta.placeholder = document.activeElement === ta
|
|
132
|
+
const hint = () => (ta.placeholder = spellProp('launcher.input', 'placeholder') || (document.activeElement === ta
|
|
117
133
|
? 'What should I go do? Paste screenshots, drop logs or videos…'
|
|
118
|
-
: `What should I go do? Press ${combo('L')} to start typing`);
|
|
134
|
+
: `What should I go do? Press ${combo('L')} to start typing`));
|
|
119
135
|
ta.addEventListener('focus', hint);
|
|
120
136
|
ta.addEventListener('blur', hint);
|
|
121
137
|
hint();
|
|
@@ -247,8 +263,7 @@ export function renderLauncher() {
|
|
|
247
263
|
});
|
|
248
264
|
});
|
|
249
265
|
paintCompare();
|
|
250
|
-
|
|
251
|
-
runnerPicker(runnerBtn, { agent: agent?.id, model: curModel, effort: curEffort, savedModel, savedEffort, places: launch.where, auto: true }, (id, model, effort) => {
|
|
266
|
+
const pickRun = (id, model, effort) => {
|
|
252
267
|
agent = harnessesAt(launch.where, { auto: true }).find((a) => a.id === id);
|
|
253
268
|
curModel = model;
|
|
254
269
|
curEffort = effort;
|
|
@@ -260,11 +275,38 @@ export function renderLauncher() {
|
|
|
260
275
|
const efforts = effort || effortsFor(id, model).length ? { ...d.efforts, [id]: effort } : d.efforts;
|
|
261
276
|
Object.assign(d, { agent: id, model, models, effort, efforts });
|
|
262
277
|
api('/config', { method: 'PATCH', body: { defaults: { agent: id, model, models, effort, efforts } } }).catch(() => {});
|
|
263
|
-
|
|
278
|
+
};
|
|
279
|
+
runnerBtn.onclick = () => runnerPicker(runnerBtn, { agent: agent?.id, model: curModel, effort: curEffort, savedModel, savedEffort, places: launch.where, auto: true }, pickRun);
|
|
280
|
+
// Spells: set the pickers (setLaunch), or launch once with other settings (launchWith).
|
|
281
|
+
box.spellSet = (v) => {
|
|
282
|
+
if (v.agent && harnessesAt(launch.where, { auto: true }).some((a) => a.id === v.agent)) pickRun(v.agent, v.model ?? savedModel(v.agent), v.effort ?? savedEffort(v.agent, v.model ?? savedModel(v.agent)));
|
|
283
|
+
else if (v.agent) toast(`${esc(agentName(v.agent))} isn’t installed here`, { tone: 'bad' });
|
|
284
|
+
if (v.mode) setMode(v.mode);
|
|
285
|
+
};
|
|
286
|
+
box.spellLaunch = (v) => go(v);
|
|
264
287
|
|
|
265
|
-
|
|
288
|
+
// over: a spell's launchWith ({ agent, model, effort, mode, text }) for this one launch.
|
|
289
|
+
const go = async (over = null) => {
|
|
290
|
+
if (over && !(over instanceof Event) && over.text && !ta.value.trim().startsWith(over.text)) ta.value = `${over.text}${ta.value.trim() ? `\n\n${ta.value.trim()}` : ''}`;
|
|
291
|
+
if (over instanceof Event) over = null;
|
|
266
292
|
const prompt = ta.value.trim();
|
|
267
293
|
if (!prompt) return ta.focus();
|
|
294
|
+
// /spell <what to change>: cast a spell instead of starting a task.
|
|
295
|
+
const spellAsk = /^\/spell(?:\s+|$)([\s\S]*)$/i.exec(prompt);
|
|
296
|
+
if (spellAsk) {
|
|
297
|
+
if (!spellAsk[1].trim()) return toast('Say what to change after /spell, like “/spell a floating button that opens my PRs”');
|
|
298
|
+
ta.value = '';
|
|
299
|
+
S.drafts.delete('home');
|
|
300
|
+
ta.dispatchEvent(new Event('input'));
|
|
301
|
+
return cast(spellAsk[1]).catch((err) => (toastError(err), (ta.value = prompt), ta.dispatchEvent(new Event('input'))));
|
|
302
|
+
}
|
|
303
|
+
// A spell's slash command, typed in the prompt box.
|
|
304
|
+
const slash = spellSlash(prompt);
|
|
305
|
+
if (slash) {
|
|
306
|
+
ta.value = '';
|
|
307
|
+
ta.dispatchEvent(new Event('input'));
|
|
308
|
+
return runAction(slash.spell, slash.cmd.do, { args: slash.args, input: ta });
|
|
309
|
+
}
|
|
268
310
|
if (isFeedback(prompt)) {
|
|
269
311
|
ta.value = '';
|
|
270
312
|
ta.dispatchEvent(new Event('input'));
|
|
@@ -277,7 +319,7 @@ export function renderLauncher() {
|
|
|
277
319
|
const off = targets.find((t) => t.m && t.m.state !== 'online');
|
|
278
320
|
if (off) return offlineToast(off.k);
|
|
279
321
|
if (needsSetup()) return nudgeSetup();
|
|
280
|
-
const wanted = [agent?.id, ...compare.map((c) => c.agent)].filter((id) => id !== 'auto'); // Auto picks from what's there
|
|
322
|
+
const wanted = [over?.agent || agent?.id, ...compare.map((c) => c.agent)].filter((id) => id !== 'auto'); // Auto picks from what's there
|
|
281
323
|
for (const t of targets) {
|
|
282
324
|
const list = agentsAt(t.k);
|
|
283
325
|
const missing = list && wanted.find((id) => !list.some((x) => x.id === id && x.available));
|
|
@@ -290,7 +332,8 @@ export function renderLauncher() {
|
|
|
290
332
|
const attachments = await tray.ids();
|
|
291
333
|
const files = tray.take();
|
|
292
334
|
const cmp = compare;
|
|
293
|
-
const runner = { agent: agent?.id || 'claude', model: curModel, effort: curEffort };
|
|
335
|
+
const runner = over?.agent ? { agent: over.agent, model: over.model ?? savedModel(over.agent), effort: over.effort ?? '' } : { agent: agent?.id || 'claude', model: curModel, effort: curEffort };
|
|
336
|
+
const launchMode = over?.mode || curMode;
|
|
294
337
|
// A repo still cloning gets a stand-in project; its card says so until the clone lands.
|
|
295
338
|
const fetching = (t) => t.k && [t.project, ...t.also].some((p) => p && !p.scratch && !pathAt(p, t.k)); // not on that machine yet
|
|
296
339
|
const pending = targets.map((t) => addPending(prompt, t.project ? t : { ...t, project: { id: t.clone.id, name: t.clone.name, path: '', color: 'slate' } }, runner, t.clone ? `Cloning ${t.clone.label}` : fetching(t) ? `Cloning ${t.project.name} on ${placeName(t.k)}` : undefined));
|
|
@@ -320,7 +363,7 @@ export function renderLauncher() {
|
|
|
320
363
|
targets.map((t) =>
|
|
321
364
|
api('/sessions', {
|
|
322
365
|
method: 'POST',
|
|
323
|
-
body: { prompt, cwd: t.project.scratch ? undefined : t.project.path, also: t.also.length ? t.also.map((p) => p.path) : undefined, scratch: t.project.scratch || undefined, machine: t.k, groupId, agent: runner.agent, mode:
|
|
366
|
+
body: { prompt, cwd: t.project.scratch ? undefined : t.project.path, also: t.also.length ? t.also.map((p) => p.path) : undefined, scratch: t.project.scratch || undefined, machine: t.k, groupId, agent: runner.agent, mode: launchMode, model: runner.model, effort: runner.effort, attachments, worktree: S.config.defaults.worktree === 'off' && !cmp.length ? false : undefined, compare: cmp.length ? cmp : undefined, subagents: team.length && canLead(runner.agent) ? team : undefined },
|
|
324
367
|
}),
|
|
325
368
|
),
|
|
326
369
|
);
|
|
@@ -347,7 +390,9 @@ export function renderLauncher() {
|
|
|
347
390
|
tray.restore(files);
|
|
348
391
|
}
|
|
349
392
|
};
|
|
350
|
-
$('#launch', box).onclick =
|
|
393
|
+
$('#launch', box).onclick = () => {
|
|
394
|
+
go();
|
|
395
|
+
};
|
|
351
396
|
// Picking other places (or a machine coming online) changes which harnesses exist: keep the
|
|
352
397
|
// pick if one of them still has it, else move to one that's installed there.
|
|
353
398
|
box.placesChanged = () => {
|
package/web/js/features/live.js
CHANGED
|
@@ -17,6 +17,7 @@ import { openSettings } from './settings.js';
|
|
|
17
17
|
import { renderPrStrip } from './ship.js';
|
|
18
18
|
import { onChildSummary } from './team.js';
|
|
19
19
|
import { onActivity } from './turns.js';
|
|
20
|
+
import { applySpells, spellEvent } from './spells.js';
|
|
20
21
|
import { loadUpdate, renderUpdate } from './update.js';
|
|
21
22
|
|
|
22
23
|
export function connect() {
|
|
@@ -37,10 +38,16 @@ export function connect() {
|
|
|
37
38
|
keepInPlace(follow ? S.nodes.get(s.id).card : feedAnchor(s.id), renderFeed);
|
|
38
39
|
if (prev?.state !== s.state || prev?.queued !== s.queued) scheduleRefetch(s.id);
|
|
39
40
|
if (JSON.stringify([prev?.pr, prev?.toShip, prev?.state, prev?.shipping]) !== JSON.stringify([s.pr, s.toShip, s.state, s.shipping])) renderPrStrip(s.id);
|
|
41
|
+
if (prev?.state !== s.state || prev?.pr?.state !== s.pr?.state) spellEvent('summary');
|
|
40
42
|
});
|
|
41
43
|
es.addEventListener('activity', (e) => onActivity(JSON.parse(e.data)));
|
|
42
44
|
es.addEventListener('turn', (e) => scheduleRefetch(JSON.parse(e.data).sessionId));
|
|
43
|
-
es.addEventListener('finished', (e) => notifyFinished(JSON.parse(e.data).session));
|
|
45
|
+
es.addEventListener('finished', (e) => (notifyFinished(JSON.parse(e.data).session), spellEvent('finished')));
|
|
46
|
+
// A spell hook's notify, from the server.
|
|
47
|
+
es.addEventListener('spell', (e) => {
|
|
48
|
+
const m = JSON.parse(e.data);
|
|
49
|
+
if (m.toast) toast(`<b>${esc(m.spell)}</b> · ${esc(m.toast)}`);
|
|
50
|
+
});
|
|
44
51
|
es.addEventListener('removed', (e) => {
|
|
45
52
|
const { id } = JSON.parse(e.data);
|
|
46
53
|
S.sessions.delete(id);
|
|
@@ -80,6 +87,7 @@ export async function loadSessions() {
|
|
|
80
87
|
|
|
81
88
|
export async function loadConfig() {
|
|
82
89
|
S.config = await api('/config');
|
|
90
|
+
applySpells();
|
|
83
91
|
renderProjects();
|
|
84
92
|
renderLaunchChips();
|
|
85
93
|
paintAutoBtn();
|
|
@@ -23,6 +23,7 @@ import { copySetupCommand, INSTALLABLE, installHarness, isInstalling, setupComma
|
|
|
23
23
|
import { clearPicks } from './transcript.js';
|
|
24
24
|
import { repaintViews } from './turns.js';
|
|
25
25
|
import { openUsage } from './usage.js';
|
|
26
|
+
import { spellsPane } from './spell-ui.js';
|
|
26
27
|
|
|
27
28
|
// A sidebar of sections and one pane at a time, like every other app's settings. Big lists
|
|
28
29
|
// (memory, automations, usage history) keep their own views; a pane only summarises and links.
|
|
@@ -34,6 +35,7 @@ const SETTINGS_PANES = [
|
|
|
34
35
|
{ id: 'machines', label: 'Machines', icon: 'server', lede: 'Other computers that run agents for you, over SSH.' },
|
|
35
36
|
{ id: 'memory', label: 'Memory', icon: 'spark', lede: "Learned from your sessions and added to every agent's prompt. What you ask always wins." },
|
|
36
37
|
{ id: 'automations', label: 'Automations', icon: 'play', lede: 'Saved prompts, and the triggers that launch them for you.' },
|
|
38
|
+
{ id: 'spells', label: 'Spells', icon: 'spark', lede: 'Changes to wisp itself: buttons, panels, blocks, looks and behaviour. Cast one, or add one a colleague sent.' },
|
|
37
39
|
{ id: 'usage', label: 'Usage', icon: 'stats', lede: 'Spend and outcomes over the last 7 days.' },
|
|
38
40
|
{ id: 'privacy', label: 'Privacy', icon: 'info', lede: 'What wisp sends about itself. Never prompts, paths or code.' },
|
|
39
41
|
];
|
|
@@ -135,6 +137,7 @@ export function openSettings(opts) {
|
|
|
135
137
|
${row('Triggers', '<span class="trigger-hint"></span>', '<button class="ghost sm" data-auto="triggers">Manage</button>')}
|
|
136
138
|
</div>
|
|
137
139
|
</section>
|
|
140
|
+
<section class="sx-pane sp-pane" data-pane="spells"></section>
|
|
138
141
|
<section class="sx-pane" data-pane="usage">
|
|
139
142
|
<div class="us-tiles sx-usage"><div class="it-loading"><span class="orb tiny"></span>Adding it up…</div></div>
|
|
140
143
|
<div class="sx-group">${row('Charts and breakdowns', 'Per day, per project and per harness · model', '<button class="ghost sm" id="openUsage">Open usage</button>')}</div>
|
|
@@ -149,6 +152,7 @@ export function openSettings(opts) {
|
|
|
149
152
|
</div>`);
|
|
150
153
|
const close = overlay(node);
|
|
151
154
|
$('.close', node).onclick = close;
|
|
155
|
+
spellsPane($('[data-pane="spells"].sx-pane', node));
|
|
152
156
|
const paintAgents = () => {
|
|
153
157
|
$('.sx-agents', node).innerHTML = S.agents.length
|
|
154
158
|
? S.agents.map((a) => {
|
|
@@ -441,6 +445,23 @@ export function openSettings(opts) {
|
|
|
441
445
|
$('#openRouter', node).onclick = () => openRouter({ host: host('auto') });
|
|
442
446
|
$('#openMemory', node).onclick = () => openMemory({ host: host('memory') });
|
|
443
447
|
$$('[data-auto]', node).forEach((b) => (b.onclick = () => openAutomations({ tab: b.dataset.auto, host: host('automations') })));
|
|
448
|
+
// Settings a spell sets are locked while it's on: the row says which spell, and links to Spells.
|
|
449
|
+
const LOCKABLE = { transcript: '#txDefault', worktree: '#wtDefault', fixConflicts: '#fixConflicts', learn: '#learn', editor: 'select[name="editor"]', notify: '#notif' };
|
|
450
|
+
const paintLocks = () => {
|
|
451
|
+
for (const n of $$('.sp-lock', node)) n.remove();
|
|
452
|
+
for (const [key, sel] of Object.entries(LOCKABLE)) {
|
|
453
|
+
const ctl = $(sel, node);
|
|
454
|
+
if (!ctl) continue;
|
|
455
|
+
const by = S.config.lockedBy?.[key];
|
|
456
|
+
ctl.disabled = !!by;
|
|
457
|
+
if (by) $('.sx-text', ctl.closest('.sx-row'))?.insertAdjacentHTML('beforeend', `<small class="sp-lock">${icon('spark')}Set by the spell <b>${esc(by.name)}</b> · <button class="link xs" data-pane-go="spells">Spells</button></small>`);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
paintLocks();
|
|
461
|
+
bus.on('config', paintLocks, { node });
|
|
462
|
+
node.addEventListener('click', (e) => {
|
|
463
|
+
if (e.target.closest('[data-pane-go]')) show(e.target.closest('[data-pane-go]').dataset.paneGo);
|
|
464
|
+
});
|
|
444
465
|
$('#openUsage', node).onclick = () => openUsage({ days: 7, host: host('usage') });
|
|
445
466
|
$('#openPhone', node).onclick = () => openPhone({ host: host('notifications') });
|
|
446
467
|
$('#openMachines', node).onclick = () => openMachines({ host: host('machines') });
|
package/web/js/features/slash.js
CHANGED
|
@@ -5,6 +5,7 @@ import { esc } from '../md.js';
|
|
|
5
5
|
import { el } from '../core/dom.js';
|
|
6
6
|
import { SLASH, slashMatches } from '../slash-text.js';
|
|
7
7
|
import { feedbackOn } from './feedback.js';
|
|
8
|
+
import { activeSpells } from './spells.js';
|
|
8
9
|
|
|
9
10
|
// Register before the box's own Enter handler: picking a command must not send it.
|
|
10
11
|
export function slashMenu(ta, host) {
|
|
@@ -13,7 +14,7 @@ export function slashMenu(ta, host) {
|
|
|
13
14
|
let items = [];
|
|
14
15
|
let sel = 0;
|
|
15
16
|
let closed = false; // Esc: hidden until the text changes
|
|
16
|
-
const commands = () => SLASH.filter((c) => c.name !== 'feedback' || feedbackOn());
|
|
17
|
+
const commands = () => [...SLASH.filter((c) => c.name !== 'feedback' || feedbackOn()), ...activeSpells().flatMap((s) => (s.slash || []).map((c) => ({ name: c.name, args: c.args || '', hint: c.hint || `From ${s.name}` })))];
|
|
17
18
|
const paint = () => {
|
|
18
19
|
const next = !closed && document.activeElement === ta ? slashMatches(ta.value, commands()) : [];
|
|
19
20
|
if (next.map((c) => c.name).join() !== items.map((c) => c.name).join()) sel = 0;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// What spell buttons do to tasks, through the same functions the rest of the page uses. Loaded on
|
|
2
|
+
// first use, so spells.js doesn't import half the app.
|
|
3
|
+
|
|
4
|
+
import { esc } from '../md.js';
|
|
5
|
+
import { api } from '../core/api.js';
|
|
6
|
+
import { $, el, icon } from '../core/dom.js';
|
|
7
|
+
import { S } from '../core/state.js';
|
|
8
|
+
import { overlay } from '../ui/overlay.js';
|
|
9
|
+
import { toast } from '../ui/toast.js';
|
|
10
|
+
import { setArchived, toggleDone } from './actions.js';
|
|
11
|
+
import { recipeParams, runRecipe } from './automations.js';
|
|
12
|
+
import { sendReply } from './composer.js';
|
|
13
|
+
import { shipDialog } from './ship.js';
|
|
14
|
+
|
|
15
|
+
// A follow-up: through the card's box when it's open (optimistic, like typing it), else straight to the API.
|
|
16
|
+
export async function sendTo(id, text) {
|
|
17
|
+
if (S.full.has(id) && S.nodes.get(id)?.body) return sendReply(id, text);
|
|
18
|
+
await api(`/sessions/${id}/reply`, { method: 'POST', body: { text } });
|
|
19
|
+
toast('Sent');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const project = () => S.config.projects.find((p) => p.id === S.filter) || null;
|
|
23
|
+
|
|
24
|
+
export async function launchTask(v, s) {
|
|
25
|
+
const cwd = v.cwd || s?.cwd || project()?.path || '';
|
|
26
|
+
const out = await api('/sessions', { method: 'POST', body: { prompt: v.prompt, ...(cwd ? { cwd } : { scratch: true }), agent: v.agent || undefined, model: v.model || undefined, effort: v.effort || undefined, mode: v.mode || undefined } });
|
|
27
|
+
toast(`Started <b>${esc(out.title)}</b>`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Rerun a task as a new one (its first request unless the spell gives a prompt), on another harness.
|
|
31
|
+
export async function rerunOn(s, v) {
|
|
32
|
+
const full = S.full.get(s.id) || (await api(`/sessions/${s.id}`));
|
|
33
|
+
const prompt = v.prompt || full.turns?.[0]?.prompt || s.title;
|
|
34
|
+
const out = await api('/sessions', { method: 'POST', body: { prompt, cwd: s.scratch ? undefined : s.cwd, scratch: s.scratch || undefined, agent: v.agent || undefined, model: v.model ?? undefined, effort: v.effort || undefined, mode: v.mode || undefined } });
|
|
35
|
+
toast(`Started <b>${esc(out.title)}</b> on ${esc(out.agent)}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function recipe(name) {
|
|
39
|
+
const loose = (x) => String(x).toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
40
|
+
const r = (S.config.recipes || []).find((x) => x.id === name || loose(x.name) === loose(name));
|
|
41
|
+
if (!r) throw new Error(`No recipe named ${name}`);
|
|
42
|
+
runRecipe({ ...r, params: recipeParams(r.prompt) });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const ship = (id) => shipDialog(id);
|
|
46
|
+
export const done = (id) => toggleDone(id);
|
|
47
|
+
export const archive = (id) => setArchived(id, true);
|
|
48
|
+
|
|
49
|
+
export async function exec(id, command) {
|
|
50
|
+
const out = await api(`/sessions/${id}/exec`, { method: 'POST', body: { command, key: `spell:${command}` } });
|
|
51
|
+
const ok = out.exitCode === 0;
|
|
52
|
+
toast(`${icon(ok ? 'check' : 'x')} <code>${esc(command)}</code> ${ok ? 'finished' : `exited ${esc(out.exitCode)}`}`, {
|
|
53
|
+
tone: ok ? '' : 'bad',
|
|
54
|
+
action: {
|
|
55
|
+
label: 'Output',
|
|
56
|
+
run: () => {
|
|
57
|
+
const node = el(`<div class="modal sp-panel"><header><h2>${icon('terminal')} ${esc(command)}</h2><button class="icon-btn close">${icon('x')}</button></header><pre class="code sp-out"><code>${esc(out.output || '(no output)')}</code></pre></div>`);
|
|
58
|
+
const close = overlay(node);
|
|
59
|
+
$('.close', node).onclick = close;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
timeout: 8000,
|
|
63
|
+
});
|
|
64
|
+
}
|