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
|
@@ -15,11 +15,13 @@ export const MAX_COMPARE = 4;
|
|
|
15
15
|
export function createLaunch(app) {
|
|
16
16
|
const { store, runner, machines } = app;
|
|
17
17
|
|
|
18
|
-
// body: { prompt, cwd, agent, mode, model, effort, attachments: [uploadId], worktree, compare,
|
|
18
|
+
// body: { prompt, cwd, scratch: true = no folder, agent, mode, model, effort, attachments: [uploadId], worktree, compare,
|
|
19
19
|
// subagents: [{ name, agent, model, effort, use }] | preset: teamId }
|
|
20
20
|
async function launch(body, origin = null) {
|
|
21
21
|
if (!String(body.prompt || '').trim()) throw bad('prompt required');
|
|
22
|
-
|
|
22
|
+
// scratch: no project folder (general questions); the task gets an empty directory of its own.
|
|
23
|
+
const scratch = body.scratch === true;
|
|
24
|
+
const cwd = scratch ? store.scratchDir(newId()) : expand(body.cwd || process.cwd());
|
|
23
25
|
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) throw bad(`Not a directory: ${cwd}`);
|
|
24
26
|
const d = store.config.defaults;
|
|
25
27
|
const preset = body.preset && store.config.teams.find((t) => t.id === body.preset);
|
|
@@ -27,17 +29,21 @@ export function createLaunch(app) {
|
|
|
27
29
|
const subagents = normalizeTeam(body.subagents ?? preset?.members, adapters);
|
|
28
30
|
// compare: more { agent, model, effort } to run the same task on, side by side. 'auto' is
|
|
29
31
|
// resolved here: the router picks a harness and model (a different one per compare run).
|
|
30
|
-
const resolved = await app.auto.resolve(body, await app.auto.agents(), { team: subagents.length > 0 });
|
|
31
|
-
|
|
32
|
+
const resolved = await app.auto.resolve(body, await app.auto.agents(), { team: subagents.length > 0, cwd });
|
|
33
|
+
// A launch routed by another wisp (this is a machine) arrives with its route: Auto's steps
|
|
34
|
+
// (check, repair) still run here, where the files are.
|
|
35
|
+
const isRoute = (x) => x && typeof x === 'object' && typeof x.key === 'string';
|
|
36
|
+
const runners = [{ agent: resolved.agent, model: resolved.model, effort: resolved.effort, route: resolved.route ?? body.route }, ...(resolved.compare || [])].slice(0, MAX_COMPARE).map((r, i) => {
|
|
32
37
|
const agent = r.agent || (i ? d.agent : '') || 'claude';
|
|
33
38
|
if (!adapters[agent]) throw bad(`unknown agent: ${agent}`);
|
|
34
|
-
return { agent, model: r.model ?? (i ? d.models?.[agent] || '' : ''), effort: /^[\w-]{1,20}$/.test(r.effort || '') ? r.effort : '', ...(r.route && { route: r.route }) };
|
|
39
|
+
return { agent, model: r.model ?? (i ? d.models?.[agent] || '' : ''), effort: /^[\w-]{1,20}$/.test(r.effort || '') ? r.effort : '', ...(isRoute(r.route) && { route: r.route }) };
|
|
35
40
|
});
|
|
41
|
+
const strategy = ['single', 'best2', 'compare'].includes(resolved.autoStrategy) ? resolved.autoStrategy : runners.length > 1 ? 'compare' : 'single';
|
|
36
42
|
const mode = MODES.has(body.mode) ? body.mode : 'auto';
|
|
37
43
|
// Worktrees are on by default in git repos (off in Plan mode: nothing gets written). Compare
|
|
38
44
|
// runs need one each, or the harnesses would edit the same files.
|
|
39
45
|
const comparing = runners.length > 1;
|
|
40
|
-
const wantTree = body.worktree ?? (comparing || (mode !== 'plan' && d.worktree !== 'off'));
|
|
46
|
+
const wantTree = !scratch && (body.worktree ?? (comparing || (mode !== 'plan' && d.worktree !== 'off')));
|
|
41
47
|
// groupId: one launch sent to several machines (or here and there) shows as one task.
|
|
42
48
|
const groupId = /^[a-f0-9]{12}$/.test(body.groupId || '') ? body.groupId : comparing ? newId() : null;
|
|
43
49
|
const attachments = app.uploads.resolve(body.attachments);
|
|
@@ -60,7 +66,7 @@ export function createLaunch(app) {
|
|
|
60
66
|
}
|
|
61
67
|
if (failed) console.warn(`wisp: no worktree for ${cwd}: ${failed.message}`);
|
|
62
68
|
}
|
|
63
|
-
return runners.map((r, i) => runner.create({ id: ids[i], prompt: body.prompt, cwd, ...r, mode, attachments, worktree: trees[i], groupId, origin, subagents }));
|
|
69
|
+
return runners.map((r, i) => runner.create({ id: ids[i], prompt: body.prompt, cwd, ...r, mode, attachments, worktree: trees[i], groupId, origin, subagents, ...(r.route && { auto: { strategy }, scout: resolved.scout || null }) }));
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
// Where a launch runs: body.machine, else a project folder that only exists on a machine
|
|
@@ -87,15 +93,18 @@ export function createLaunch(app) {
|
|
|
87
93
|
// back through the summaries it streams (auto.signals), so one router learns from every place.
|
|
88
94
|
const routed = await app.auto.resolve(body, machines.agentsOf(mid) || [], { team: !!(body.subagents?.length || body.preset) });
|
|
89
95
|
const routes = [routed.route, ...(routed.compare || []).map((c) => c.route)];
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
// The machine runs Auto's check and repair on these picks (an older wisp there ignores them).
|
|
97
|
+
const sendBody = { ...routed };
|
|
98
|
+
delete sendBody.scout;
|
|
92
99
|
if (!sendBody.compare?.length && !body.compare) delete sendBody.compare;
|
|
93
100
|
const out = await machines.launch(mid, { ...sendBody, attachments: await pushUploads(mid, body.attachments), ...(origin && { origin }) });
|
|
94
101
|
(out.group || [out]).forEach((x, i) => routes[i] && app.router.record(qualify(x.id, mid), routes[i]));
|
|
95
|
-
const known = store.projectFor(out.cwd, mid);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
const known = out.scratch || store.projectFor(out.cwd, mid);
|
|
103
|
+
if (!out.scratch) {
|
|
104
|
+
(known || store.addProject(out.cwd, { machine: mid })).lastUsedAt = Date.now();
|
|
105
|
+
store.saveConfig();
|
|
106
|
+
if (!known) app.broadcast('config', {});
|
|
107
|
+
}
|
|
99
108
|
return (out.group || [out]).map((x) => machines.summary(mid, x));
|
|
100
109
|
}
|
|
101
110
|
|
package/server/services/ship.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
// Ship it: whether a worktree session has something to ship, the draft commit message and PR
|
|
2
|
-
// text, and following open PRs (checks, reviews, merges) so their cards stay current.
|
|
2
|
+
// text, and following open PRs (checks, reviews, merges) so their cards stay current. A PR that
|
|
3
|
+
// conflicts with its base gets fixed when the user clicks Fix conflicts on its row (or on its own
|
|
4
|
+
// with defaults.fixConflicts: 'on'): the task is sent a follow-up to merge the base in, and the
|
|
5
|
+
// result is pushed to the PR when it's done.
|
|
3
6
|
|
|
4
7
|
import fs from 'node:fs';
|
|
5
|
-
import { worktreeStatus, prView } from '../git.js';
|
|
8
|
+
import { worktreeStatus, prView, ship, baseMerged, conflictsWithBase } from '../git.js';
|
|
6
9
|
import { blocksToMarkdown } from '../render.js';
|
|
7
10
|
import { summarize } from '../runner.js';
|
|
8
11
|
|
|
9
12
|
const PR_POLL_MS = 60_000;
|
|
13
|
+
const PR_FAST_POLL_MS = 10_000; // while GitHub is still working something out: checks running, mergeability unknown, a push just landed
|
|
14
|
+
const PR_TICK_MS = 5_000;
|
|
10
15
|
const SHIP_RECHECK_MS = 5_000;
|
|
16
|
+
const MAX_CONFLICT_FIXES = 3; // per task, so a base that keeps moving can't run up the bill
|
|
17
|
+
|
|
18
|
+
export const conflictPrompt = (s) => {
|
|
19
|
+
const { remote, base } = s.worktree;
|
|
20
|
+
return [
|
|
21
|
+
`PR #${s.pr.number} (${s.pr.url}) can no longer merge into \`${base}\`: changes that landed there since conflict with this branch. Bring the branch up to date:`,
|
|
22
|
+
'',
|
|
23
|
+
`1. \`git fetch ${remote}\`, then \`git merge ${remote}/${base}\`. Don't rebase: the branch is already pushed.`,
|
|
24
|
+
`2. Resolve every conflict, keeping the intent of both sides: what landed on \`${base}\` and what this task changed. Don't drop either side's changes to make a conflict go away.`,
|
|
25
|
+
'3. Run the checks that are cheap here (tests, type-check, build) on the merged result and fix what the merge broke.',
|
|
26
|
+
"4. Finish with `git commit --no-edit`. Don't push: wisp pushes to the PR when you're done.",
|
|
27
|
+
'',
|
|
28
|
+
'If a conflict needs a decision only the user can make, ask them.',
|
|
29
|
+
].join('\n');
|
|
30
|
+
};
|
|
11
31
|
|
|
12
32
|
export function createShip(app) {
|
|
13
33
|
const { store, runner } = app;
|
|
@@ -48,14 +68,73 @@ export function createShip(app) {
|
|
|
48
68
|
const pr = await prView(dir, prev.number);
|
|
49
69
|
const changed = (k) => JSON.stringify(k(pr)) !== JSON.stringify(k(prev));
|
|
50
70
|
s.pr = pr;
|
|
71
|
+
if (s.conflictFix && !pr.conflict && !['fixing', 'pushing'].includes(s.conflictFix.step)) delete s.conflictFix;
|
|
51
72
|
if (pr.state !== prev.state) app.auto.signals(summarize(s, store));
|
|
52
73
|
// Things worth coming back for: CI finished either way, a review landed, it merged or closed.
|
|
53
74
|
const settledChecks = (x) => (x.checks.pending ? 'pending' : x.checks.fail ? 'fail' : 'pass');
|
|
54
75
|
if (changed((x) => x.state) || changed((x) => x.review) || (changed(settledChecks) && settledChecks(pr) !== 'pending')) s.unread = true;
|
|
55
76
|
app.sessionChanged(s);
|
|
77
|
+
await fixConflicts(s).catch(() => {});
|
|
56
78
|
return pr;
|
|
57
79
|
};
|
|
58
80
|
|
|
81
|
+
// The PR conflicts with its base: send the task a follow-up to merge the base in. Only when the
|
|
82
|
+
// user clicks Fix conflicts (force), unless defaults.fixConflicts is 'on': then on its own, once
|
|
83
|
+
// per PR head, so a fix that didn't take isn't retried until something changes the branch.
|
|
84
|
+
const starting = new Set(); // a poll and a refresh at once must not both send it
|
|
85
|
+
const fixConflicts = async (s, { force = false } = {}) => {
|
|
86
|
+
const pr = s.pr;
|
|
87
|
+
if ((!force && store.config.defaults.fixConflicts !== 'on') || pr?.state !== 'OPEN' || !pr.conflict || !s.worktree?.remote) return;
|
|
88
|
+
if (runner.live.has(s.id) || s.shipping || s.ask || s.archived || starting.has(s.id)) return;
|
|
89
|
+
const tries = s.conflictFix?.tries || 0;
|
|
90
|
+
if (!force && (s.conflictFix?.head === pr.head || tries >= MAX_CONFLICT_FIXES)) return;
|
|
91
|
+
// The user saw the conflict and asked: no fetch first, the follow-up starts at once.
|
|
92
|
+
if (!force) {
|
|
93
|
+
starting.add(s.id);
|
|
94
|
+
try {
|
|
95
|
+
if ((await conflictsWithBase(s.worktree)) === false || runner.live.has(s.id)) return;
|
|
96
|
+
} finally {
|
|
97
|
+
starting.delete(s.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
s.conflictFix = { step: 'fixing', head: pr.head, tries: tries + 1, startedAt: Date.now() };
|
|
101
|
+
await app.launcher.replyTo(s, conflictPrompt(s), [], true);
|
|
102
|
+
app.telemetry?.track?.('conflict_fix', { agent: s.agent });
|
|
103
|
+
app.sessionChanged(s, { now: true });
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// The fix-up turn ended: push it to the PR if the base really is merged in, else leave the task
|
|
107
|
+
// for the user with why.
|
|
108
|
+
const finishFix = async (s, { interrupted, errorText }) => {
|
|
109
|
+
const fix = s.conflictFix;
|
|
110
|
+
if (fix?.step !== 'fixing') return;
|
|
111
|
+
const fail = (why) => {
|
|
112
|
+
Object.assign(fix, { step: 'failed', why });
|
|
113
|
+
s.unread = true;
|
|
114
|
+
app.sessionChanged(s, { now: true });
|
|
115
|
+
};
|
|
116
|
+
if (interrupted || s.queue?.length) return fail('Stopped: the task moved on before the conflicts were fixed.');
|
|
117
|
+
if (errorText) return fail(`The agent run failed: ${errorText}`.slice(0, 300));
|
|
118
|
+
const check = await baseMerged(s.worktree).catch((err) => ({ ok: false, why: err.message }));
|
|
119
|
+
if (!check.ok) return fail(check.why);
|
|
120
|
+
fix.step = 'pushing';
|
|
121
|
+
s.shipping = { step: 'start', update: true, startedAt: Date.now() };
|
|
122
|
+
app.sessionChanged(s);
|
|
123
|
+
const onStep = (step) => ((s.shipping = { ...s.shipping, step }), app.sessionChanged(s));
|
|
124
|
+
try {
|
|
125
|
+
s.pr = await ship(s.worktree, { title: `Merge ${s.worktree.remote}/${s.worktree.base} into ${s.worktree.branch}`, pr: s.pr, onStep });
|
|
126
|
+
s.pushedAt = Date.now();
|
|
127
|
+
fix.step = 'done';
|
|
128
|
+
} catch (err) {
|
|
129
|
+
Object.assign(fix, { step: 'failed', why: `Push failed: ${err.message}` });
|
|
130
|
+
s.unread = true;
|
|
131
|
+
} finally {
|
|
132
|
+
delete s.shipping;
|
|
133
|
+
app.sessionChanged(s, { now: true });
|
|
134
|
+
}
|
|
135
|
+
recheck(s);
|
|
136
|
+
};
|
|
137
|
+
|
|
59
138
|
// Default commit message and PR text, from the session's latest presentation.
|
|
60
139
|
const draft = (s) => {
|
|
61
140
|
const turn = s.turns.findLast((t) => t.status === 'done') || s.turns.at(-1);
|
|
@@ -70,13 +149,35 @@ export function createShip(app) {
|
|
|
70
149
|
return { title: title.slice(0, 100), body: `${body}\n\n_Shipped from [wisp](https://github.com/M0xoo/wisp)._`.trim() };
|
|
71
150
|
};
|
|
72
151
|
|
|
73
|
-
runner.on('ended', ({ session }) =>
|
|
152
|
+
runner.on('ended', ({ session, interrupted, errorText }) => {
|
|
153
|
+
const s = store.get(session.id);
|
|
154
|
+
recheck(s);
|
|
155
|
+
if (s?.conflictFix?.step === 'fixing') finishFix(s, { interrupted, errorText }).catch(() => {});
|
|
156
|
+
});
|
|
157
|
+
for (const s of store.all()) {
|
|
158
|
+
delete s.shipping; // a ship cut short by a restart isn't running anymore
|
|
159
|
+
if (s.conflictFix?.step === 'pushing') Object.assign(s.conflictFix, { step: 'failed', why: 'wisp restarted while pushing the fix.' });
|
|
160
|
+
}
|
|
74
161
|
for (const s of store.all()) if (s.worktree && !s.archived && s.pr?.state !== 'MERGED') recheck(s);
|
|
75
162
|
|
|
163
|
+
// Open PRs are checked every minute, and every few seconds while their state is still settling,
|
|
164
|
+
// so a check finishing or a pushed fix clearing the conflict shows up quickly.
|
|
165
|
+
const settling = (s) => {
|
|
166
|
+
const pr = s.pr;
|
|
167
|
+
return !!(pr.checks?.pending || pr.mergeState === 'UNKNOWN' || ['pushing', 'done'].includes(s.conflictFix?.step) || Date.now() - (s.pushedAt || 0) < 5 * 60_000);
|
|
168
|
+
};
|
|
169
|
+
let polling = false;
|
|
76
170
|
const timer = setInterval(async () => {
|
|
77
|
-
|
|
78
|
-
|
|
171
|
+
if (polling) return;
|
|
172
|
+
polling = true;
|
|
173
|
+
try {
|
|
174
|
+
const due = store.all().filter((s) => s.pr?.state === 'OPEN' && Date.now() - (s.pr.checkedAt || 0) >= (settling(s) ? PR_FAST_POLL_MS : PR_POLL_MS));
|
|
175
|
+
await Promise.all(due.map((s) => refreshPr(s).catch(() => {})));
|
|
176
|
+
} finally {
|
|
177
|
+
polling = false;
|
|
178
|
+
}
|
|
179
|
+
}, PR_TICK_MS);
|
|
79
180
|
timer.unref();
|
|
80
181
|
|
|
81
|
-
return { recheck, refreshPr, draft, stop: () => clearInterval(timer) };
|
|
182
|
+
return { recheck, refreshPr, fixConflicts, draft, stop: () => clearInterval(timer) };
|
|
82
183
|
}
|
package/server/store.js
CHANGED
|
@@ -88,6 +88,7 @@ export class Store {
|
|
|
88
88
|
|
|
89
89
|
// machine: a project on another computer (paths there are POSIX); null = this one.
|
|
90
90
|
projectFor(cwd = '', machine = null) {
|
|
91
|
+
if (!machine && this.isScratch(cwd)) return null; // never inside a project, even one at ~
|
|
91
92
|
const sep = machine ? '/' : path.sep;
|
|
92
93
|
let best = null;
|
|
93
94
|
for (const p of this.config.projects)
|
|
@@ -112,7 +113,19 @@ export class Store {
|
|
|
112
113
|
return project;
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// Folder-less tasks run in a throwaway directory of their own under ~/.wisp/scratch.
|
|
117
|
+
scratchDir(id) {
|
|
118
|
+
const dir = path.join(this.home, 'scratch', id);
|
|
119
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
120
|
+
return dir;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isScratch(dir = '') {
|
|
124
|
+
return String(dir).startsWith(path.join(this.home, 'scratch') + path.sep);
|
|
125
|
+
}
|
|
126
|
+
|
|
115
127
|
touchProject(dir) {
|
|
128
|
+
if (this.isScratch(dir)) return null;
|
|
116
129
|
const p = this.projectFor(dir) || this.addProject(dir);
|
|
117
130
|
p.lastUsedAt = Date.now();
|
|
118
131
|
this.saveConfig();
|
|
@@ -147,6 +160,8 @@ export class Store {
|
|
|
147
160
|
|
|
148
161
|
delete(id) {
|
|
149
162
|
clearTimeout(this.timers.get(id));
|
|
163
|
+
const cwd = this.sessions.get(id)?.cwd;
|
|
164
|
+
if (this.isScratch(cwd)) fs.rmSync(cwd, { recursive: true, force: true }); // a folder-less task's files go with it
|
|
150
165
|
this.sessions.delete(id);
|
|
151
166
|
fs.rmSync(path.join(this.dir, `${id}.json`), { force: true });
|
|
152
167
|
fs.rmSync(path.join(this.shots, id), { recursive: true, force: true });
|
package/web/css/auto.css
CHANGED
|
@@ -27,3 +27,25 @@
|
|
|
27
27
|
.ro-recent .dim { color: var(--faint); }
|
|
28
28
|
.ro-foot { display: flex; align-items: center; gap: 10px; margin-top: 16px; }
|
|
29
29
|
.ro-foot .hint { margin: 0; }
|
|
30
|
+
|
|
31
|
+
/* Auto's steps after a run: check, repair, hand-over, best of two */
|
|
32
|
+
.pill.auto-ck { gap: 4px; }
|
|
33
|
+
.pill.auto-ck .i, .pill.auto-pick .i { width: 11px; height: 11px; }
|
|
34
|
+
.pill.auto-ck.ck-pass { --tone: var(--good); color: var(--good); }
|
|
35
|
+
.pill.auto-ck.ck-fail { --tone: var(--bad); color: var(--bad); }
|
|
36
|
+
.pill.auto-ck.ck-unknown { color: var(--muted); }
|
|
37
|
+
.pill.auto-pick { gap: 4px; color: var(--accent-ink); }
|
|
38
|
+
.pill.auto-handed { color: var(--muted); }
|
|
39
|
+
.ask.by-wisp { background: color-mix(in srgb, var(--accent) 6%, var(--bg-2)); }
|
|
40
|
+
.ask-by { display: flex; align-items: center; gap: 5px; margin-bottom: 4px; font-size: 11.5px; font-weight: 500; color: var(--accent-ink); }
|
|
41
|
+
.ask-by .i { width: 12px; height: 12px; }
|
|
42
|
+
.t-by { font-weight: 600; color: var(--accent-ink); }
|
|
43
|
+
.tests-by { margin-left: 8px; padding: 1px 6px; border-radius: 5px; font-size: 11px; font-weight: 500; vertical-align: middle; color: var(--accent-ink); background: var(--accent-soft); }
|
|
44
|
+
.ro-repairs, .ro-scout { flex: none; }
|
|
45
|
+
.ro-tests { display: flex; flex-direction: column; }
|
|
46
|
+
.ro-test { display: flex; align-items: center; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--line); }
|
|
47
|
+
.ro-test .us-proj { width: 160px; flex: none; font-size: 12.5px; }
|
|
48
|
+
.ro-test .input { flex: 1; min-width: 0; font-size: 12px; }
|
|
49
|
+
.router-modal .hint code { font-size: 11px; padding: 0 4px; border-radius: 4px; background: var(--bg-2); }
|
|
50
|
+
.ro-tier { margin-left: 6px; padding: 0 5px; border-radius: 4px; font-style: normal; font-size: 10.5px; color: var(--faint); background: var(--bg-2); }
|
|
51
|
+
.ro-tier.t3 { color: var(--accent-ink); background: var(--accent-soft); }
|
package/web/css/launcher.css
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
while it's open so switching between the two harness pickers doesn't flash the ring off and on. */
|
|
5
5
|
.launcher:is(:focus-within, :has(.open)) { border-color: color-mix(in srgb, var(--accent) 55%, var(--line-2)); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
6
6
|
.launcher.drop, .composer.drop { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
7
|
+
/* index.html's first paint: same height as the real launcher, so nothing jumps when it fills in */
|
|
8
|
+
.launcher.skel .l-projects { height: 35px; }
|
|
9
|
+
.launcher.skel .l-bar { height: 38px; }
|
|
10
|
+
.launcher.skel .l-recipes { height: 40px; }
|
|
11
|
+
@container launcher (max-width: 480px) { .launcher.skel .l-recipes { height: 90px; } } /* its hint wraps on phones */
|
|
7
12
|
.l-projects { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 0 2px 8px; }
|
|
8
13
|
/* no harness yet: one quiet line offering to install opencode */
|
|
9
14
|
.l-setup { display: flex; align-items: center; gap: 8px; margin: 0 0 4px; padding: 6px 6px 6px 10px; border-radius: 9px; background: var(--accent-soft); font-size: 13px; color: var(--ink-2); }
|
|
@@ -24,8 +29,20 @@
|
|
|
24
29
|
}
|
|
25
30
|
.lp-chip:hover { border-color: var(--line-2); background: var(--surface-2); }
|
|
26
31
|
.lp-chip.on { background: var(--bg-2); border-color: var(--line-2); color: var(--ink); font-weight: 500; }
|
|
32
|
+
#wtBtn[hidden] { display: none; }
|
|
33
|
+
.lp-chip.none .i { width: 13px; height: 13px; color: var(--muted); }
|
|
34
|
+
.lp-chip.none.on .i { color: var(--ink); }
|
|
27
35
|
.lp-chip.add { color: var(--muted); border-style: dashed; }
|
|
36
|
+
.lp-chip.cloning .orb.live { --s: 11px; margin: 0; }
|
|
37
|
+
.lp-chip .lp-note { color: var(--faint); font-weight: 400; font-size: 11.5px; }
|
|
28
38
|
.lp-chip.add .i { width: 14px; height: 14px; }
|
|
39
|
+
/* "+N": the projects that didn't get a chip, one search away */
|
|
40
|
+
.lp-chip.more { gap: 5px; padding: 0 9px; color: var(--muted); }
|
|
41
|
+
.lp-chip.more:hover, .lp-chip.more.open { color: var(--ink); border-color: var(--line-2); }
|
|
42
|
+
.lp-chip.more .i { width: 12px; height: 12px; }
|
|
43
|
+
.l-projects .lp-chip[hidden] { display: none; }
|
|
44
|
+
/* narrow, with "+N" (whose list has No folder too): it goes icon-only unless picked */
|
|
45
|
+
@container launcher (max-width: 520px) { .l-projects:has(.more:not([hidden])) .lp-chip.none:not(.on) span { display: none; } }
|
|
29
46
|
/* recipes: one-click prompts under the launch bar */
|
|
30
47
|
.l-recipes { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 8px -12px -10px; padding: 7px 14px 8px; border-top: 1px solid var(--line); }
|
|
31
48
|
.l-recipes .l-in { display: inline-flex; align-items: center; gap: 5px; }
|
package/web/css/layout.css
CHANGED
|
@@ -45,8 +45,16 @@
|
|
|
45
45
|
.dot.bad { background: var(--bad); }
|
|
46
46
|
|
|
47
47
|
/* project filter chips in the top bar */
|
|
48
|
-
.projects { flex: 1; min-width: 0; display: flex; align-items: center; gap: 4px;
|
|
49
|
-
|
|
48
|
+
.projects { flex: 1; min-width: 0; display: flex; align-items: center; gap: 4px; }
|
|
49
|
+
/* only whole chips: those that don't fit the bar fold into "+N" (projects.js fits them) */
|
|
50
|
+
.p-scroll { flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 4px; overflow: hidden; }
|
|
51
|
+
.p-chip[hidden], .p-chip > [hidden], .p-scroll[hidden] { display: none; }
|
|
52
|
+
.p-chip.more.solo { min-width: 0; flex: 0 1 auto; gap: 6px; padding-left: 10px; color: var(--ink); font-weight: 500; }
|
|
53
|
+
.p-chip.more.solo .p-more-n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
54
|
+
.p-chip.more { flex: none; gap: 4px; padding: 0 7px 0 9px; color: var(--muted); border-color: var(--line); }
|
|
55
|
+
.p-chip.more:hover, .p-chip.more.open { border-color: var(--line-2); color: var(--ink); }
|
|
56
|
+
.p-chip.more .chev { width: 11px; height: 11px; transform: rotate(90deg); transition: transform 0.2s var(--ease); }
|
|
57
|
+
.p-chip.more.open .chev { transform: rotate(-90deg); }
|
|
50
58
|
.p-chip {
|
|
51
59
|
display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border-radius: 99px; flex: none;
|
|
52
60
|
font-size: 12.5px; color: var(--muted); border: 1px solid transparent; transition: background 0.15s, border-color 0.15s, color 0.15s;
|
package/web/css/modals.css
CHANGED
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
.pname:focus { box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
44
44
|
.ppath { font: 11.5px var(--mono); color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
45
45
|
.pcount { font-size: 12px; color: var(--muted); }
|
|
46
|
+
.discover h4 { display: flex; align-items: center; gap: 6px; }
|
|
47
|
+
.discover h4 .i { width: 14px; height: 14px; }
|
|
48
|
+
.discover h4 .muted { font-weight: 400; }
|
|
49
|
+
.prow.cloning .ppath { color: var(--muted); }
|
|
50
|
+
.discover h4 .link { margin-left: auto; font-size: 12px; }
|
|
51
|
+
.prow.cloning .orb.live { margin: 0; }
|
|
52
|
+
.hint .clonedir { width: 220px; height: 24px; padding: 0 7px; margin: 0 4px; border-radius: 7px; border: 1px solid var(--line-2); background: var(--surface); color: var(--ink); font-size: 12px; outline: 0; }
|
|
53
|
+
.hint .clonedir:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
46
54
|
.found { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 6px; }
|
|
47
55
|
.found-item { display: grid; grid-template-columns: 16px 1fr; column-gap: 8px; text-align: left; padding: 8px 10px; border-radius: 11px; border: 1px solid var(--line); transition: all 0.15s; }
|
|
48
56
|
.found-item:hover { border-color: var(--accent); background: var(--accent-soft); }
|
package/web/css/palette.css
CHANGED
|
@@ -53,6 +53,28 @@
|
|
|
53
53
|
.menu { position: fixed; z-index: 40; min-width: 220px; padding: 5px; border-radius: 13px; background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-2); animation: pop 0.2s var(--ease); }
|
|
54
54
|
.menu button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 10px; border-radius: 8px; font-size: 13.5px; color: var(--ink-2); }
|
|
55
55
|
.menu button:hover { background: var(--bg-2); color: var(--ink); }
|
|
56
|
+
/* the project list: search over every project, rows below, fixed extras at the bottom */
|
|
57
|
+
.menu.pm { display: flex; flex-direction: column; width: 340px; max-width: calc(100vw - 16px); padding: 0; overflow: hidden; }
|
|
58
|
+
.menu .pm-search, .menu .pm-search:is(:hover, :focus-within) {
|
|
59
|
+
flex: none; height: 42px; padding: 0 12px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: none; box-shadow: none; font-size: 13.5px;
|
|
60
|
+
}
|
|
61
|
+
.pm-list { max-height: min(340px, 48vh); overflow-y: auto; padding: 5px; }
|
|
62
|
+
.pm-extras { flex: none; padding: 5px; border-top: 1px solid var(--line); }
|
|
63
|
+
.menu .pm-row { gap: 8px; padding: 6px 9px; text-align: left; }
|
|
64
|
+
.menu .pm-row:hover { background: none; }
|
|
65
|
+
.menu .pm-row.act { background: var(--bg-2); color: var(--ink); }
|
|
66
|
+
.pm-row .p-dot { flex: none; }
|
|
67
|
+
.pm-name { flex: none; max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
68
|
+
.pm-row.on .pm-name { font-weight: 500; color: var(--ink); }
|
|
69
|
+
.pm-name mark { background: none; color: var(--accent-ink); font-weight: 600; }
|
|
70
|
+
.pm-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; color: var(--faint); }
|
|
71
|
+
.pm-row .pm-where { width: 11px; height: 11px; color: var(--muted); flex: none; }
|
|
72
|
+
.pm-row .p-need { flex: none; }
|
|
73
|
+
.pm-row .p-run { flex: none; }
|
|
74
|
+
.pm-row .pm-check { flex: none; width: 14px; height: 14px; color: var(--accent); visibility: hidden; }
|
|
75
|
+
.pm-row.on .pm-check { visibility: visible; }
|
|
76
|
+
.pm-x .i { width: 14px; height: 14px; color: var(--muted); }
|
|
77
|
+
.pm-empty { padding: 18px 10px; text-align: center; font-size: 13px; color: var(--muted); }
|
|
56
78
|
.menu .danger:hover { color: var(--bad); background: var(--del-bg); }
|
|
57
79
|
.menu-kbd { margin-left: auto; }
|
|
58
80
|
|
package/web/css/ship.css
CHANGED
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
.pr-line { color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
64
64
|
.pr-strip .ghost .i { width: 13px; height: 13px; }
|
|
65
65
|
.pr-strip .pr-merge { color: var(--good); font-weight: 500; }
|
|
66
|
+
.pr-strip .pr-fix { color: var(--bad); font-weight: 500; }
|
|
67
|
+
.pr-strip > .ghost { flex: none; white-space: nowrap; }
|
|
68
|
+
.pr-strip [data-pr="refresh"].busy .i { animation: pr-spin 0.8s linear infinite; }
|
|
69
|
+
@keyframes pr-spin { to { transform: rotate(-360deg); } }
|
|
66
70
|
@keyframes pulse { 50% { opacity: 0.35; } }
|
|
67
71
|
|
|
68
72
|
.ship-modal, .run-modal { width: min(620px, 100%); }
|
|
@@ -93,3 +97,43 @@
|
|
|
93
97
|
.cmp-body { flex: 1; min-height: 160px; max-height: 58vh; overflow: auto; padding: 12px; background: var(--surface); }
|
|
94
98
|
.cmp-foot { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-top: 1px solid var(--line); }
|
|
95
99
|
|
|
100
|
+
|
|
101
|
+
/* Ship it in flight: the PR row turns into a track of stages that fills in as they run */
|
|
102
|
+
.pr-strip.ship-run {
|
|
103
|
+
--tone: var(--accent); position: relative; overflow: hidden; animation: ship-in 0.32s var(--ease);
|
|
104
|
+
background: color-mix(in srgb, var(--tone) 6%, transparent); border-color: color-mix(in srgb, var(--tone) 22%, transparent);
|
|
105
|
+
}
|
|
106
|
+
.ship-icon { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 6px; flex: none; color: var(--accent-ink); background: color-mix(in srgb, var(--tone) 14%, transparent); }
|
|
107
|
+
.ship-icon .i { width: 12px; height: 12px; animation: ship-fly 1.1s ease-in-out infinite; }
|
|
108
|
+
.ship-what { font-weight: 500; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
109
|
+
.ship-what .dots::after { content: '…'; animation: pulse 1.1s ease-in-out infinite; }
|
|
110
|
+
.ship-track { display: flex; align-items: center; gap: 4px; margin: 0 0 0 4px; padding: 0; list-style: none; font-size: 11.5px; color: var(--faint); flex: none; }
|
|
111
|
+
.ship-track li { display: inline-flex; align-items: center; gap: 4px; transition: color 0.25s; }
|
|
112
|
+
.ship-track li + li::before { content: ''; width: 14px; height: 1.5px; margin-right: 2px; border-radius: 1px; background: var(--line-2); transition: background 0.3s; }
|
|
113
|
+
.ship-track li.done::before, .ship-track li.now::before, .ship-track li.bad::before { background: var(--tone); }
|
|
114
|
+
.ship-track i { display: grid; place-items: center; width: 13px; height: 13px; border-radius: 50%; border: 1.5px solid var(--line-2); transition: background 0.25s, border-color 0.25s; }
|
|
115
|
+
.ship-track i .i { width: 8px; height: 8px; color: #fff; opacity: 0; transform: scale(0.4); transition: opacity 0.2s, transform 0.25s var(--ease); }
|
|
116
|
+
.ship-track li.done { color: var(--ink-2); }
|
|
117
|
+
.ship-track li.done i { background: var(--good); border-color: var(--good); }
|
|
118
|
+
.ship-track li.done i .i { opacity: 1; transform: none; }
|
|
119
|
+
.ship-track li.now { color: var(--ink); font-weight: 500; }
|
|
120
|
+
.ship-track li.now i { border-color: var(--tone); border-top-color: transparent; animation: spin 0.8s linear infinite; }
|
|
121
|
+
.ship-track li.bad { color: var(--bad); font-weight: 500; }
|
|
122
|
+
.ship-track li.bad i { background: var(--bad); border-color: var(--bad); }
|
|
123
|
+
.ship-bar { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: linear-gradient(90deg, transparent, var(--tone), transparent) 0 0 / 40% 100% no-repeat; animation: ship-sweep 1.3s ease-in-out infinite; }
|
|
124
|
+
.ship-run.failed .ship-bar, .ship-run.failed .ship-icon .i { animation: none; }
|
|
125
|
+
.ship-run.failed .ship-bar { display: none; }
|
|
126
|
+
.ship-run.failed .ship-icon { color: var(--bad); }
|
|
127
|
+
.ship-run.failed { animation: nudge 0.4s ease; }
|
|
128
|
+
.ship-err { font-weight: 400; color: var(--ink-2); }
|
|
129
|
+
.ship-acts { display: inline-flex; gap: 2px; }
|
|
130
|
+
.ship-acts .i { width: 13px; height: 13px; }
|
|
131
|
+
.pr-strip.landed { animation: ship-land 0.9s var(--ease); }
|
|
132
|
+
.pill.ship-pill { --tone: var(--accent); }
|
|
133
|
+
.pill.ship-pill i { animation: pulse 1.1s ease-in-out infinite; }
|
|
134
|
+
@keyframes ship-in { from { opacity: 0; transform: translateY(6px) scale(0.98); } }
|
|
135
|
+
@keyframes ship-fly { 50% { transform: translateY(-2.5px); } }
|
|
136
|
+
@keyframes ship-sweep { from { background-position: -40% 0; } to { background-position: 140% 0; } }
|
|
137
|
+
@keyframes ship-land { from { box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 30%, transparent); transform: scale(1.015); } }
|
|
138
|
+
@media (max-width: 640px) { .ship-track span { display: none; } }
|
|
139
|
+
@media (prefers-reduced-motion: reduce) { .ship-run, .ship-run *, .pr-strip.landed { animation: none !important; } }
|
package/web/index.html
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
<symbol id="i-dots" viewBox="0 0 24 24"><circle cx="6" cy="12" r="1.6" fill="currentColor"/><circle cx="12" cy="12" r="1.6" fill="currentColor"/><circle cx="18" cy="12" r="1.6" fill="currentColor"/></symbol>
|
|
40
40
|
<symbol id="i-copy" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M16 8V6a2 2 0 00-2-2H6a2 2 0 00-2 2v8a2 2 0 002 2h2" fill="none" stroke="currentColor" stroke-width="1.7"/></symbol>
|
|
41
41
|
<symbol id="i-branch" viewBox="0 0 24 24"><circle cx="6" cy="5" r="2.2" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="6" cy="19" r="2.2" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="18" cy="7" r="2.2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M6 7.2v9.6M18 9.2c0 5-12 3-12 7.6" fill="none" stroke="currentColor" stroke-width="1.7"/></symbol>
|
|
42
|
+
<symbol id="i-github" viewBox="0 0 24 24"><path d="M12 2.5a9.5 9.5 0 00-3 18.52c.48.09.65-.2.65-.46v-1.62c-2.64.57-3.2-1.27-3.2-1.27-.43-1.1-1.05-1.39-1.05-1.39-.86-.59.07-.58.07-.58.95.07 1.45.98 1.45.98.85 1.45 2.22 1.03 2.76.79.09-.61.33-1.03.6-1.27-2.1-.24-4.32-1.05-4.32-4.68 0-1.03.37-1.88.98-2.54-.1-.24-.42-1.2.09-2.5 0 0 .8-.26 2.61.97a9.1 9.1 0 014.76 0c1.81-1.23 2.61-.97 2.61-.97.51 1.3.19 2.26.09 2.5.61.66.98 1.51.98 2.54 0 3.64-2.22 4.44-4.34 4.67.34.3.64.88.64 1.77v2.63c0 .26.17.56.66.46A9.5 9.5 0 0012 2.5z" fill="currentColor"/></symbol>
|
|
42
43
|
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 11v6M12 7.5v.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></symbol>
|
|
43
44
|
<symbol id="i-warn" viewBox="0 0 24 24"><path d="M12 3.5l9.5 16.5h-19L12 3.5z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M12 10v4.5M12 17.2v.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></symbol>
|
|
44
45
|
<symbol id="i-ask" viewBox="0 0 24 24"><path d="M4 5.5A2.5 2.5 0 016.5 3h11A2.5 2.5 0 0120 5.5v8a2.5 2.5 0 01-2.5 2.5H10l-4.5 4v-4h0A1.5 1.5 0 014 14.5v-9z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M10 8.2a2 2 0 113 1.7c-.7.4-1 .8-1 1.6M12 13.2v.3" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M4 20l1-4L16 5l3 3L8 19l-4 1z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
|
|
47
48
|
<symbol id="i-spark" viewBox="0 0 24 24"><path d="M12 3l1.8 5.4L19 10l-5.2 1.6L12 17l-1.8-5.4L5 10l5.2-1.6L12 3zM18.5 15.5l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8.8-2.2z" fill="currentColor"/></symbol>
|
|
48
49
|
<symbol id="i-stop" viewBox="0 0 24 24"><rect x="6.5" y="6.5" width="11" height="11" rx="2" fill="currentColor"/></symbol>
|
|
50
|
+
<symbol id="i-merge" viewBox="0 0 24 24"><circle cx="6" cy="5" r="2.2" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="6" cy="19" r="2.2" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="18" cy="15" r="2.2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M6 7.2v9.6M6 7.2c0 4.4 4.4 7.8 9.8 7.8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
|
49
51
|
<symbol id="i-send" viewBox="0 0 24 24"><path d="M12 19V5m-6 6l6-6 6 6" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
|
50
52
|
<symbol id="i-enter" viewBox="0 0 24 24"><path d="M19 5v7a3 3 0 01-3 3H5m4-4l-4 4 4 4" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
|
51
53
|
<symbol id="i-queue" viewBox="0 0 24 24"><path d="M4 6h12M4 12h12M4 18h7M17 15v6m-3-3h6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></symbol>
|
|
@@ -102,13 +104,22 @@
|
|
|
102
104
|
</button>
|
|
103
105
|
</header>
|
|
104
106
|
<div class="feed-wrap">
|
|
105
|
-
<div class="launch-slot" id="launchSlot"
|
|
107
|
+
<div class="launch-slot" id="launchSlot">
|
|
108
|
+
<!-- First paint, before the scripts load: the launcher's shape, and a box you can already type
|
|
109
|
+
in. renderLauncher() fills in the rest and keeps what was typed. -->
|
|
110
|
+
<section class="launcher card skel" id="launcher">
|
|
111
|
+
<div class="l-projects"><span class="l-in">in</span></div>
|
|
112
|
+
<textarea id="prompt" rows="2" spellcheck="false" autofocus placeholder="What should I go do? Paste screenshots, drop logs or videos…"></textarea>
|
|
113
|
+
<div class="l-bar"><span class="grow"></span><button class="send" tabindex="-1" aria-hidden="true"><svg class="i"><use href="#i-enter"/></svg></button></div>
|
|
114
|
+
<div class="l-recipes"></div>
|
|
115
|
+
</section>
|
|
116
|
+
</div>
|
|
106
117
|
<div class="feed-head" id="feedHead"></div>
|
|
107
118
|
<div class="feed" id="feed">
|
|
108
|
-
<section class="f-group needs" id="secNeeds"><h4>Needs you<span class="n"></span></h4><div class="f-list"></div></section>
|
|
109
|
-
<section class="f-group working" id="secWorking"><h4>Working<span class="n"></span></h4><div class="f-list"></div></section>
|
|
110
|
-
<section class="f-group done" id="secDone"><h4 class="toggle-done">Done<span class="n"></span><svg class="i chev"><use href="#i-chevron"/></svg></h4><div class="f-list"></div></section>
|
|
111
|
-
<section class="f-group done archived" id="secArchived"><h4 class="toggle-done">Archived<span class="n"></span><svg class="i chev"><use href="#i-chevron"/></svg></h4><div class="f-list"></div></section>
|
|
119
|
+
<section class="f-group needs" id="secNeeds" hidden><h4>Needs you<span class="n"></span></h4><div class="f-list"></div></section>
|
|
120
|
+
<section class="f-group working" id="secWorking" hidden><h4>Working<span class="n"></span></h4><div class="f-list"></div></section>
|
|
121
|
+
<section class="f-group done" id="secDone" hidden><h4 class="toggle-done">Done<span class="n"></span><svg class="i chev"><use href="#i-chevron"/></svg></h4><div class="f-list"></div></section>
|
|
122
|
+
<section class="f-group done archived" id="secArchived" hidden><h4 class="toggle-done">Archived<span class="n"></span><svg class="i chev"><use href="#i-chevron"/></svg></h4><div class="f-list"></div></section>
|
|
112
123
|
</div>
|
|
113
124
|
<div class="feed-empty" id="feedEmpty" hidden></div>
|
|
114
125
|
</div>
|
package/web/js/app.js
CHANGED
|
@@ -7,7 +7,7 @@ import { api } from './core/api.js';
|
|
|
7
7
|
import * as bus from './core/bus.js';
|
|
8
8
|
import { $, $$ } from './core/dom.js';
|
|
9
9
|
import { ago } from './core/format.js';
|
|
10
|
-
import { S, save } from './core/state.js';
|
|
10
|
+
import { S, save, setAgents } from './core/state.js';
|
|
11
11
|
import { combo } from './ui/kbd.js';
|
|
12
12
|
import { openAutomations } from './features/automations.js';
|
|
13
13
|
import { openPalette } from './features/commands.js';
|
|
@@ -70,12 +70,23 @@ window.addEventListener('focus', () => {
|
|
|
70
70
|
for (const id of S.open) if (S.sessions.get(id)?.unread) scheduleRefetch(id);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
// The launcher paints as soon as config and machines are in (local, fast), from the last harness
|
|
74
|
+
// list seen. Detecting harnesses can take seconds on a cold start: it repaints the launcher when
|
|
75
|
+
// it lands, only if the list changed.
|
|
73
76
|
(async () => {
|
|
74
|
-
|
|
77
|
+
const agents = api('/agents').catch(() => null);
|
|
78
|
+
const sessions = loadSessions();
|
|
79
|
+
await Promise.all([loadConfig(), loadMachines()]);
|
|
75
80
|
renderLauncher();
|
|
76
81
|
bus.emit('agents');
|
|
82
|
+
$('#prompt')?.focus();
|
|
83
|
+
await sessions;
|
|
77
84
|
connect();
|
|
78
85
|
route();
|
|
79
|
-
$('#prompt')?.focus();
|
|
80
86
|
telemetryNotice();
|
|
87
|
+
const list = await agents;
|
|
88
|
+
if (!list || JSON.stringify(list) === JSON.stringify(S.agents)) return;
|
|
89
|
+
setAgents(list);
|
|
90
|
+
renderLauncher();
|
|
91
|
+
bus.emit('agents');
|
|
81
92
|
})();
|
package/web/js/blocks.js
CHANGED
|
@@ -512,7 +512,7 @@ R.tests = (b, ctx) => {
|
|
|
512
512
|
</svg>
|
|
513
513
|
<div class="ring-label">${icon(ok ? 'check' : 'x')}</div>
|
|
514
514
|
<div class="tests-text">
|
|
515
|
-
<div class="verdict">${ok ? 'All green' : `${failed} failing`}</div>
|
|
515
|
+
<div class="verdict">${ok ? 'All green' : `${failed} failing`}${b.by === 'wisp' ? `<span class="tests-by" title="wisp ran the project's tests itself after the agent finished${b.note ? `: ${esc(b.note)}` : ''}">Run by wisp</span>` : ''}</div>
|
|
516
516
|
<div class="counts"><span class="pass"><b class="v" data-v="${passed}"></b> passed</span>${failed ? `<span class="fail"><b class="v" data-v="${failed}"></b> failed</span>` : ''}${skipped ? `<span class="skip"><b class="v" data-v="${skipped}"></b> skipped</span>` : ''}${b.durationMs ? `<span class="dur">${fmtDuration(b.durationMs)}</span>` : ''}</div>
|
|
517
517
|
${cmd ? `<div class="tests-cmd"><code class="cmdline">$ ${esc(cmd)}</code>${ctx.exec ? `<button class="ghost xs run-tests" title="Re-run ${esc(cmd)}">${icon('rerun')}<span>Re-run</span></button>` : ''}</div>` : ''}
|
|
518
518
|
</div>
|
package/web/js/core/state.js
CHANGED
|
@@ -22,7 +22,7 @@ export const S = {
|
|
|
22
22
|
tasks: new Map(), // compare task key -> { card, head, runs, lead }
|
|
23
23
|
focus: null, // id of the keyboard-focused item
|
|
24
24
|
config: { defaults: {}, projects: [] },
|
|
25
|
-
agents: [],
|
|
25
|
+
agents: load('wisp.agents', []), // the last list seen, so the launcher paints before detection answers
|
|
26
26
|
machines: [], // other computers over SSH: GET /api/machines, kept fresh by the 'machines' event
|
|
27
27
|
filter: load('wisp.filter', 'all'),
|
|
28
28
|
showDone: load('wisp.showDone', false),
|
|
@@ -32,6 +32,7 @@ export const S = {
|
|
|
32
32
|
replyRunners: new Map(), // session id → { agent, model, effort } its next follow-up switches to
|
|
33
33
|
};
|
|
34
34
|
export const saveOpen = () => save('wisp.open', [...S.open]);
|
|
35
|
+
export const setAgents = (list) => ((S.agents = list), save('wisp.agents', list));
|
|
35
36
|
|
|
36
37
|
// Where the launcher sends a prompt: '' = this computer, else machine ids. Several run in parallel,
|
|
37
38
|
// each in the project picked for it (launch.picks[place]), and show up as one task.
|
|
@@ -8,7 +8,7 @@ import { $, el, icon } from '../core/dom.js';
|
|
|
8
8
|
import { S } from '../core/state.js';
|
|
9
9
|
import { keyHint } from '../ui/kbd.js';
|
|
10
10
|
import { overlay } from '../ui/overlay.js';
|
|
11
|
-
import { popover } from '../ui/popover.js';
|
|
11
|
+
import { clampToViewport, popover } from '../ui/popover.js';
|
|
12
12
|
import { toast, toastError, tryApi } from '../ui/toast.js';
|
|
13
13
|
import { autosize } from './attachments.js';
|
|
14
14
|
import { saveAsRecipe } from './automations.js';
|
|
@@ -208,9 +208,7 @@ export function taskMenu(id, e) {
|
|
|
208
208
|
if (a === 'delete') deleteSession(id);
|
|
209
209
|
};
|
|
210
210
|
const close = popover(menu);
|
|
211
|
-
|
|
212
|
-
if (r.right > innerWidth - 8) menu.style.left = `${innerWidth - r.width - 8}px`;
|
|
213
|
-
if (r.bottom > innerHeight - 8) menu.style.top = `${innerHeight - r.height - 8}px`;
|
|
211
|
+
clampToViewport(menu, e.clientX, e.clientY);
|
|
214
212
|
}
|
|
215
213
|
|
|
216
214
|
export async function deleteSession(id) {
|