wisp-agents 0.16.0 → 0.18.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 +38 -1
- package/README.md +6 -5
- package/bin/wisp.js +4 -2
- package/package.json +2 -2
- package/server/adapters/antigravity.js +5 -1
- package/server/adapters/claude.js +48 -5
- package/server/adapters/codex.js +3 -0
- package/server/adapters/cursor.js +2 -0
- package/server/adapters/grok.js +15 -0
- package/server/adapters/index.js +9 -1
- package/server/adapters/opencode.js +31 -6
- package/server/client.js +1 -1
- package/server/git.js +114 -2
- package/server/handoff.js +2 -1
- package/server/machines.js +2 -2
- package/server/mcp-ask.js +113 -8
- package/server/mentions.js +39 -0
- package/server/openapi.js +21 -3
- package/server/protocol.js +8 -1
- package/server/render.js +3 -1
- package/server/routes/internal.js +121 -1
- package/server/routes/meta.js +1 -1
- package/server/routes/sessions.js +12 -0
- package/server/routes/settings.js +51 -4
- package/server/routes/ship.js +24 -2
- package/server/runner.js +142 -22
- package/server/services/auto.js +12 -1
- package/server/services/launch.js +12 -8
- package/server/services/phone.js +1 -1
- package/server/services/ship.js +83 -4
- package/server/store.js +23 -2
- package/server/triggers.js +1 -1
- package/server/usage.js +94 -0
- package/server/welcome.js +2 -2
- package/web/css/composer.css +22 -0
- package/web/css/feed.css +27 -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 +42 -2
- package/web/index.html +16 -5
- package/web/js/app.js +14 -3
- package/web/js/blocks.js +46 -0
- package/web/js/core/state.js +2 -1
- package/web/js/features/actions.js +9 -4
- package/web/js/features/automations.js +1 -1
- package/web/js/features/clones.js +79 -0
- package/web/js/features/compare.js +2 -1
- package/web/js/features/composer.js +11 -7
- package/web/js/features/feed.js +3 -3
- package/web/js/features/launcher.js +150 -28
- package/web/js/features/live.js +3 -3
- package/web/js/features/mentions.js +125 -0
- package/web/js/features/notify.js +1 -1
- package/web/js/features/project-list.js +99 -0
- package/web/js/features/projects.js +218 -34
- package/web/js/features/runners.js +3 -1
- package/web/js/features/settings.js +16 -2
- package/web/js/features/setup.js +3 -3
- package/web/js/features/ship.js +100 -24
- package/web/js/features/turns.js +44 -13
- package/web/js/mention-text.js +59 -0
package/server/usage.js
CHANGED
|
@@ -77,3 +77,97 @@ export function usage(sessions, { days = 30, now = Date.now(), project = () => n
|
|
|
77
77
|
runners: sorted(byRunner),
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
// Tool calls across sessions, for the agent-facing `wisp_stats` tool: how often each kind of
|
|
82
|
+
// action ran (run, read, edit, search, web, …), per harness, project and day. Kinds are wisp's
|
|
83
|
+
// normalized activity kinds, not the harness's own tool names. Spend and success come from usage().
|
|
84
|
+
const NOT_TOOLS = new Set(['text', 'error', 'answer']);
|
|
85
|
+
const tally = () => ({ calls: 0, kinds: {} });
|
|
86
|
+
const count = (b, kind) => {
|
|
87
|
+
b.calls++;
|
|
88
|
+
b.kinds[kind] = (b.kinds[kind] || 0) + 1;
|
|
89
|
+
};
|
|
90
|
+
const ranked = (m) => [...m.values()].sort((a, b) => b.calls - a.calls);
|
|
91
|
+
|
|
92
|
+
// filter: { agent, project } — agent is a harness id, project a project id or folder name.
|
|
93
|
+
export function toolStats(sessions, { days = 30, now = Date.now(), project = () => null, filter = {} } = {}) {
|
|
94
|
+
const keep = sessions
|
|
95
|
+
.filter((s) => {
|
|
96
|
+
if (!filter.project) return true;
|
|
97
|
+
const p = project(s);
|
|
98
|
+
return p?.id === filter.project || (p?.name || String(s.cwd || '').split(/[\\/]/).pop()) === filter.project;
|
|
99
|
+
})
|
|
100
|
+
.map((s) => (filter.agent ? { ...s, turns: (s.turns || []).filter((t) => (t.agent || s.agent) === filter.agent) } : s));
|
|
101
|
+
const u = usage(keep, { days, now, project });
|
|
102
|
+
const since = u.since;
|
|
103
|
+
|
|
104
|
+
const total = tally();
|
|
105
|
+
const byAgent = new Map();
|
|
106
|
+
const byProject = new Map();
|
|
107
|
+
const byDay = new Map(u.daily.map((d) => [d.date, { date: d.date, ...tally() }]));
|
|
108
|
+
for (const s of keep) {
|
|
109
|
+
if (s.agent === 'wisp') continue;
|
|
110
|
+
const p = project(s);
|
|
111
|
+
for (const t of s.turns || []) {
|
|
112
|
+
if (!t.startedAt || t.startedAt < since || t.status === 'running') continue;
|
|
113
|
+
const agent = t.agent || s.agent;
|
|
114
|
+
const pk = p?.id || s.cwd;
|
|
115
|
+
if (!byAgent.has(agent)) byAgent.set(agent, { agent, ...tally() });
|
|
116
|
+
if (!byProject.has(pk)) byProject.set(pk, { id: p?.id || null, name: p?.name || String(s.cwd || '').split(/[\\/]/).pop(), ...tally() });
|
|
117
|
+
for (const a of t.activity || []) {
|
|
118
|
+
if (!a?.kind || NOT_TOOLS.has(a.kind)) continue;
|
|
119
|
+
count(total, a.kind);
|
|
120
|
+
count(byAgent.get(agent), a.kind);
|
|
121
|
+
count(byProject.get(pk), a.kind);
|
|
122
|
+
const d = byDay.get(dayKey(a.t || t.startedAt));
|
|
123
|
+
if (d) count(d, a.kind);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const outcome = (b) => ({ turns: b.turns, cost: b.cost, priced: b.priced, successRate: b.successRate });
|
|
128
|
+
const runs = new Map();
|
|
129
|
+
for (const r of u.runners) {
|
|
130
|
+
const b = runs.get(r.agent) || { turns: 0, done: 0, cost: 0, priced: 0 };
|
|
131
|
+
runs.set(r.agent, { turns: b.turns + r.turns, done: b.done + r.done, cost: b.cost + r.cost, priced: b.priced + r.priced });
|
|
132
|
+
}
|
|
133
|
+
const withRuns = (a) => {
|
|
134
|
+
const r = runs.get(a.agent) || { turns: 0, done: 0, cost: 0, priced: 0 };
|
|
135
|
+
return { ...a, turns: r.turns, cost: Math.round(r.cost * 10000) / 10000, priced: r.priced, successRate: r.turns ? r.done / r.turns : null };
|
|
136
|
+
};
|
|
137
|
+
const projectRuns = new Map(u.projects.map((p) => [p.id || p.name, p]));
|
|
138
|
+
return {
|
|
139
|
+
days: u.days,
|
|
140
|
+
since,
|
|
141
|
+
filter: { agent: filter.agent || null, project: filter.project || null },
|
|
142
|
+
total: { ...total, sessions: u.total.sessions, ...outcome(u.total), inputTokens: u.total.inputTokens, outputTokens: u.total.outputTokens, questions: u.total.questions },
|
|
143
|
+
kinds: Object.entries(total.kinds).map(([kind, calls]) => ({ kind, calls })).sort((a, b) => b.calls - a.calls),
|
|
144
|
+
agents: ranked(byAgent).map(withRuns),
|
|
145
|
+
projects: ranked(byProject).map((p) => ({ ...p, ...outcome(projectRuns.get(p.id || p.name) || { turns: 0, cost: 0, priced: 0, successRate: null }) })),
|
|
146
|
+
daily: [...byDay.values()],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// toolStats() as markdown for an agent to read.
|
|
151
|
+
export function toolStatsText(st) {
|
|
152
|
+
const pct = (r) => (r === null || r === undefined ? '—' : `${Math.round(r * 100)}%`);
|
|
153
|
+
const usd = (b) => (b.priced ? `$${b.cost.toFixed(2)}${b.priced < b.turns ? ` (${b.priced}/${b.turns} turns priced)` : ''}` : '—');
|
|
154
|
+
const kinds = (k) => Object.entries(k).sort((a, b) => b[1] - a[1]).map(([n, c]) => `${n} ${c}`).join(', ') || '—';
|
|
155
|
+
const scope = [st.filter.agent && `harness ${st.filter.agent}`, st.filter.project && `project ${st.filter.project}`].filter(Boolean).join(', ');
|
|
156
|
+
const t = st.total;
|
|
157
|
+
const out = [
|
|
158
|
+
`## wisp tool stats — last ${st.days} day${st.days === 1 ? '' : 's'}${scope ? ` (${scope})` : ''}`,
|
|
159
|
+
`${t.calls} tool calls in ${t.turns} turns across ${t.sessions} sessions. Success rate ${pct(t.successRate)}, cost ${usd(t)}, ${t.questions} questions to the user, ${t.inputTokens} input / ${t.outputTokens} output tokens.`,
|
|
160
|
+
'',
|
|
161
|
+
'Kinds are wisp’s categories, not harness tool names: run = shell commands, read / edit / search = files, web = fetch and search, agent = subagents, tool = other (MCP) tools, ask = questions to the user, wait = waiting on the user or a background task.',
|
|
162
|
+
];
|
|
163
|
+
if (!t.turns) return [...out, '', 'No finished turns in this range.'].join('\n');
|
|
164
|
+
out.push('', '### By kind', '| kind | calls | share |', '|---|---:|---:|');
|
|
165
|
+
for (const k of st.kinds) out.push(`| ${k.kind} | ${k.calls} | ${t.calls ? Math.round((k.calls / t.calls) * 100) : 0}% |`);
|
|
166
|
+
out.push('', '### By harness', '| harness | calls | turns | success | cost | top kinds |', '|---|---:|---:|---:|---:|---|');
|
|
167
|
+
for (const a of st.agents) out.push(`| ${a.agent} | ${a.calls} | ${a.turns} | ${pct(a.successRate)} | ${usd(a)} | ${kinds(a.kinds)} |`);
|
|
168
|
+
out.push('', '### By project', '| project | calls | turns | success | cost | top kinds |', '|---|---:|---:|---:|---:|---|');
|
|
169
|
+
for (const p of st.projects) out.push(`| ${p.name} | ${p.calls} | ${p.turns} | ${pct(p.successRate)} | ${usd(p)} | ${kinds(p.kinds)} |`);
|
|
170
|
+
out.push('', '### By day (days with activity)', '| day | calls | kinds |', '|---|---:|---|');
|
|
171
|
+
for (const d of st.daily.filter((d) => d.calls)) out.push(`| ${d.date} | ${d.calls} | ${kinds(d.kinds)} |`);
|
|
172
|
+
return out.join('\n');
|
|
173
|
+
}
|
package/server/welcome.js
CHANGED
|
@@ -129,8 +129,8 @@ export function welcomeSession() {
|
|
|
129
129
|
title: 'Permission modes (pick per launch)',
|
|
130
130
|
items: [
|
|
131
131
|
{ text: 'Plan — read-only, the agent proposes', done: true },
|
|
132
|
-
{ text: 'Edits — the agent may change files,
|
|
133
|
-
{ text: 'Full auto — no permission prompts at all;
|
|
132
|
+
{ text: 'Edits — the agent may change files; anything else it needs, you approve', done: true },
|
|
133
|
+
{ text: 'Full auto — no permission prompts at all; the default, each task in its own worktree', done: true },
|
|
134
134
|
],
|
|
135
135
|
},
|
|
136
136
|
{
|
package/web/css/composer.css
CHANGED
|
@@ -60,3 +60,25 @@
|
|
|
60
60
|
.c-runner.changed .rb-logo { margin-right: 4px; }
|
|
61
61
|
.c-runner:focus-visible { outline: 0; box-shadow: 0 0 0 2px var(--accent-soft), 0 0 0 1px var(--accent); }
|
|
62
62
|
.cr-label { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
|
63
|
+
|
|
64
|
+
/* ---------- task mentions (features/mentions.js) ---------- */
|
|
65
|
+
/* In a prompt box: the textarea's own text sits over a mirror that paints a chip under each @Title. */
|
|
66
|
+
.mn-wrap { position: relative; }
|
|
67
|
+
.mn-wrap > textarea { position: relative; z-index: 1; }
|
|
68
|
+
.mn-mirror {
|
|
69
|
+
position: absolute; inset: 0; z-index: 0; margin: 0; overflow: hidden; pointer-events: none;
|
|
70
|
+
border-style: solid; border-color: transparent; color: transparent;
|
|
71
|
+
}
|
|
72
|
+
.mn-wrap:not(.has-mn) .mn-mirror { display: none; }
|
|
73
|
+
.mn-mirror mark.mn {
|
|
74
|
+
color: transparent; border-radius: 4px;
|
|
75
|
+
background: color-mix(in srgb, var(--accent) 20%, transparent);
|
|
76
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
|
|
77
|
+
}
|
|
78
|
+
.launcher.docked:not(:focus-within):not(:has(.open)) .mn-wrap { flex: 1; min-width: 0; }
|
|
79
|
+
/* In a sent prompt: the same chip, with the task's title; a link opens that task. */
|
|
80
|
+
:is(a, span).mn {
|
|
81
|
+
padding: 0 5px; border-radius: 5px; font-weight: 500; color: var(--accent-ink); text-decoration: none;
|
|
82
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent); box-decoration-break: clone; -webkit-box-decoration-break: clone;
|
|
83
|
+
}
|
|
84
|
+
a.mn:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
|
package/web/css/feed.css
CHANGED
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
.it-bar { display: flex; align-items: center; gap: 12px; padding: 8px 10px 8px 14px; }
|
|
97
97
|
.it-meta { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; }
|
|
98
98
|
.it-actions { display: flex; gap: 2px; flex: none; }
|
|
99
|
+
.it-done:hover { color: var(--good); }
|
|
99
100
|
.it-body .earlier { display: block; margin: 4px 0 0 18px; }
|
|
100
101
|
.it-dock { padding: 4px 14px 14px; }
|
|
101
102
|
|
|
@@ -206,3 +207,29 @@
|
|
|
206
207
|
.q-own { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
|
|
207
208
|
.ask-card.sent { opacity: 0.5; pointer-events: none; }
|
|
208
209
|
|
|
210
|
+
/* approvals: live (paused on a tool call) and after the fact (what the permissions blocked) */
|
|
211
|
+
.ask-card.perm .q-head { color: var(--warn); }
|
|
212
|
+
.ask-card.perm .q-text code { font-size: 0.86em; }
|
|
213
|
+
.perm-detail {
|
|
214
|
+
margin: -4px 0 12px; padding: 9px 12px; max-height: 180px; overflow: auto; border-radius: 8px;
|
|
215
|
+
background: var(--surface-2); border: 1px solid var(--line); font: 12.5px/1.5 var(--mono); white-space: pre-wrap; word-break: break-word;
|
|
216
|
+
}
|
|
217
|
+
.ask-card.perm .opt.go { border-color: color-mix(in srgb, var(--good) 55%, var(--line-2)); }
|
|
218
|
+
.ask-card.perm .opt.no { color: var(--ink-2); }
|
|
219
|
+
.b-blocked {
|
|
220
|
+
padding: 14px 16px; border-radius: var(--radius); background: var(--surface);
|
|
221
|
+
border: 1px solid color-mix(in srgb, var(--warn) 55%, var(--line));
|
|
222
|
+
}
|
|
223
|
+
.b-blocked.resolved { background: var(--surface-2); border-color: var(--line); }
|
|
224
|
+
.b-blocked .q-head { margin-bottom: 8px; }
|
|
225
|
+
.b-blocked.resolved .q-head { color: var(--muted); }
|
|
226
|
+
.bl-state { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-weight: 500; color: var(--muted); }
|
|
227
|
+
.bl-state svg { width: 13px; height: 13px; }
|
|
228
|
+
.bl-items { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
|
|
229
|
+
.bl-items li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; min-width: 0; }
|
|
230
|
+
.bl-tool { font: 500 11px var(--mono); padding: 1px 6px; border-radius: 5px; background: var(--bg-2); color: var(--ink-2); flex: none; }
|
|
231
|
+
.bl-items code { font: 12.5px/1.45 var(--mono); color: var(--ink); word-break: break-all; min-width: 0; }
|
|
232
|
+
.bl-items small { flex-basis: 100%; color: var(--muted); font-size: 12.5px; }
|
|
233
|
+
.bl-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin-top: 12px; }
|
|
234
|
+
.bl-note { font-size: 12px; color: var(--muted); }
|
|
235
|
+
|
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
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
.pill.pr-merged, .pr-strip.pr-merged { --tone: #8250df; }
|
|
54
54
|
.pill.pr-closed, .pr-strip.pr-closed { --tone: var(--faint); }
|
|
55
55
|
.pr-strip {
|
|
56
|
-
display: flex; align-items: center; gap: 8px; margin:
|
|
56
|
+
display: flex; align-items: center; gap: 8px; margin: 8px 14px 4px; padding: 5px 6px 5px 10px; border-radius: 9px;
|
|
57
57
|
font-size: 12.5px; background: color-mix(in srgb, var(--tone) 7%, transparent); border: 1px solid color-mix(in srgb, var(--tone) 18%, transparent);
|
|
58
58
|
}
|
|
59
59
|
.pr-strip[hidden] { display: none; }
|
|
60
60
|
.pr-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--tone); flex: none; }
|
|
61
61
|
.pr-strip.pr-pending .pr-dot { animation: pulse 1.4s ease-in-out infinite; }
|
|
62
|
-
.pr-num { font-weight: 600; color: var(--ink); }
|
|
62
|
+
.pr-num { font-weight: 600; color: var(--ink); white-space: nowrap; }
|
|
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; }
|
|
@@ -93,3 +93,43 @@
|
|
|
93
93
|
.cmp-body { flex: 1; min-height: 160px; max-height: 58vh; overflow: auto; padding: 12px; background: var(--surface); }
|
|
94
94
|
.cmp-foot { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-top: 1px solid var(--line); }
|
|
95
95
|
|
|
96
|
+
|
|
97
|
+
/* Ship it in flight: the PR row turns into a track of stages that fills in as they run */
|
|
98
|
+
.pr-strip.ship-run {
|
|
99
|
+
--tone: var(--accent); position: relative; overflow: hidden; animation: ship-in 0.32s var(--ease);
|
|
100
|
+
background: color-mix(in srgb, var(--tone) 6%, transparent); border-color: color-mix(in srgb, var(--tone) 22%, transparent);
|
|
101
|
+
}
|
|
102
|
+
.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); }
|
|
103
|
+
.ship-icon .i { width: 12px; height: 12px; animation: ship-fly 1.1s ease-in-out infinite; }
|
|
104
|
+
.ship-what { font-weight: 500; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
105
|
+
.ship-what .dots::after { content: '…'; animation: pulse 1.1s ease-in-out infinite; }
|
|
106
|
+
.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; }
|
|
107
|
+
.ship-track li { display: inline-flex; align-items: center; gap: 4px; transition: color 0.25s; }
|
|
108
|
+
.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; }
|
|
109
|
+
.ship-track li.done::before, .ship-track li.now::before, .ship-track li.bad::before { background: var(--tone); }
|
|
110
|
+
.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; }
|
|
111
|
+
.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); }
|
|
112
|
+
.ship-track li.done { color: var(--ink-2); }
|
|
113
|
+
.ship-track li.done i { background: var(--good); border-color: var(--good); }
|
|
114
|
+
.ship-track li.done i .i { opacity: 1; transform: none; }
|
|
115
|
+
.ship-track li.now { color: var(--ink); font-weight: 500; }
|
|
116
|
+
.ship-track li.now i { border-color: var(--tone); border-top-color: transparent; animation: spin 0.8s linear infinite; }
|
|
117
|
+
.ship-track li.bad { color: var(--bad); font-weight: 500; }
|
|
118
|
+
.ship-track li.bad i { background: var(--bad); border-color: var(--bad); }
|
|
119
|
+
.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; }
|
|
120
|
+
.ship-run.failed .ship-bar, .ship-run.failed .ship-icon .i { animation: none; }
|
|
121
|
+
.ship-run.failed .ship-bar { display: none; }
|
|
122
|
+
.ship-run.failed .ship-icon { color: var(--bad); }
|
|
123
|
+
.ship-run.failed { animation: nudge 0.4s ease; }
|
|
124
|
+
.ship-err { font-weight: 400; color: var(--ink-2); }
|
|
125
|
+
.ship-acts { display: inline-flex; gap: 2px; }
|
|
126
|
+
.ship-acts .i { width: 13px; height: 13px; }
|
|
127
|
+
.pr-strip.landed { animation: ship-land 0.9s var(--ease); }
|
|
128
|
+
.pill.ship-pill { --tone: var(--accent); }
|
|
129
|
+
.pill.ship-pill i { animation: pulse 1.1s ease-in-out infinite; }
|
|
130
|
+
@keyframes ship-in { from { opacity: 0; transform: translateY(6px) scale(0.98); } }
|
|
131
|
+
@keyframes ship-fly { 50% { transform: translate(2px, -2px); } }
|
|
132
|
+
@keyframes ship-sweep { from { background-position: -40% 0; } to { background-position: 140% 0; } }
|
|
133
|
+
@keyframes ship-land { from { box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 30%, transparent); transform: scale(1.015); } }
|
|
134
|
+
@media (max-width: 640px) { .ship-track span { display: none; } }
|
|
135
|
+
@media (prefers-reduced-motion: reduce) { .ship-run, .ship-run *, .pr-strip.landed { animation: none !important; } }
|
package/web/index.html
CHANGED
|
@@ -39,9 +39,11 @@
|
|
|
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
|
+
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="5" y="10.5" width="14" height="9.5" rx="2.2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M8.5 10.5V8a3.5 3.5 0 017 0v2.5M12 14.3v2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
|
45
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>
|
|
46
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>
|
|
47
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>
|
|
@@ -101,13 +103,22 @@
|
|
|
101
103
|
</button>
|
|
102
104
|
</header>
|
|
103
105
|
<div class="feed-wrap">
|
|
104
|
-
<div class="launch-slot" id="launchSlot"
|
|
106
|
+
<div class="launch-slot" id="launchSlot">
|
|
107
|
+
<!-- First paint, before the scripts load: the launcher's shape, and a box you can already type
|
|
108
|
+
in. renderLauncher() fills in the rest and keeps what was typed. -->
|
|
109
|
+
<section class="launcher card skel" id="launcher">
|
|
110
|
+
<div class="l-projects"><span class="l-in">in</span></div>
|
|
111
|
+
<textarea id="prompt" rows="2" spellcheck="false" autofocus placeholder="What should I go do? Paste screenshots, drop logs or videos…"></textarea>
|
|
112
|
+
<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>
|
|
113
|
+
<div class="l-recipes"></div>
|
|
114
|
+
</section>
|
|
115
|
+
</div>
|
|
105
116
|
<div class="feed-head" id="feedHead"></div>
|
|
106
117
|
<div class="feed" id="feed">
|
|
107
|
-
<section class="f-group needs" id="secNeeds"><h4>Needs you<span class="n"></span></h4><div class="f-list"></div></section>
|
|
108
|
-
<section class="f-group working" id="secWorking"><h4>Working<span class="n"></span></h4><div class="f-list"></div></section>
|
|
109
|
-
<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>
|
|
110
|
-
<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>
|
|
118
|
+
<section class="f-group needs" id="secNeeds" hidden><h4>Needs you<span class="n"></span></h4><div class="f-list"></div></section>
|
|
119
|
+
<section class="f-group working" id="secWorking" hidden><h4>Working<span class="n"></span></h4><div class="f-list"></div></section>
|
|
120
|
+
<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>
|
|
121
|
+
<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>
|
|
111
122
|
</div>
|
|
112
123
|
<div class="feed-empty" id="feedEmpty" hidden></div>
|
|
113
124
|
</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
|
@@ -788,6 +788,52 @@ R.question = (b, ctx) => {
|
|
|
788
788
|
return node;
|
|
789
789
|
};
|
|
790
790
|
|
|
791
|
+
// What the turn's permissions refused. On the latest turn: Allow & retry (exactly these where the
|
|
792
|
+
// harness can scope a grant, else one follow-up in Full auto) or Deny.
|
|
793
|
+
R.blocked = (b, ctx) => {
|
|
794
|
+
const items = (b.items || []).filter((i) => i && typeof i === 'object');
|
|
795
|
+
const t = ctx.turn || {};
|
|
796
|
+
const scoped = !!t.blocked?.length && t.blocked.every((i) => i.rule !== undefined);
|
|
797
|
+
const open = ctx.isLatest && !t.approval && !!ctx.approve && ctx.session?.state !== 'running';
|
|
798
|
+
const state = (a) => (a === 'allowed' ? `${icon('check')}Allowed and retried` : a === 'denied' ? `${icon('x')}Denied` : '');
|
|
799
|
+
const node = el(`
|
|
800
|
+
<div class="b-blocked ${open ? '' : 'resolved'}">
|
|
801
|
+
<div class="q-head">${icon('lock')}<span>Blocked by permissions</span>${t.approval ? `<span class="bl-state">${state(t.approval)}</span>` : ''}</div>
|
|
802
|
+
<ul class="bl-items">${items
|
|
803
|
+
.map((i) => `<li><span class="bl-tool">${esc(i.tool || 'tool')}</span>${i.detail ? `<code>${esc(i.detail)}</code>` : ''}${i.why ? `<small>${inlineMd(i.why)}</small>` : ''}</li>`)
|
|
804
|
+
.join('')}</ul>
|
|
805
|
+
${open ? `<div class="bl-foot">
|
|
806
|
+
<button class="primary sm bl-allow">${icon('check')}Allow & retry</button>
|
|
807
|
+
<button class="ghost sm bl-deny">Deny</button>
|
|
808
|
+
<span class="bl-note">${scoped ? 'Allows exactly these, for the retry only.' : 'This harness can’t allow single actions, so the retry runs in Full auto (that follow-up only).'}</span>
|
|
809
|
+
</div>` : ''}
|
|
810
|
+
</div>`);
|
|
811
|
+
if (!open) return node;
|
|
812
|
+
const decide = async (allow) => {
|
|
813
|
+
// Instant: the card settles at once and goes back if the server says no.
|
|
814
|
+
const foot = node.querySelector('.bl-foot');
|
|
815
|
+
const head = node.querySelector('.q-head');
|
|
816
|
+
const tag = el(`<span class="bl-state">${state(allow ? 'allowed' : 'denied')}</span>`);
|
|
817
|
+
node.classList.add('resolved');
|
|
818
|
+
foot.hidden = true;
|
|
819
|
+
head.append(tag);
|
|
820
|
+
try {
|
|
821
|
+
await ctx.approve(allow);
|
|
822
|
+
} catch {
|
|
823
|
+
node.classList.remove('resolved');
|
|
824
|
+
foot.hidden = false;
|
|
825
|
+
tag.remove();
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
node.querySelector('.bl-allow').onclick = () => {
|
|
829
|
+
decide(true);
|
|
830
|
+
};
|
|
831
|
+
node.querySelector('.bl-deny').onclick = () => {
|
|
832
|
+
decide(false);
|
|
833
|
+
};
|
|
834
|
+
return node;
|
|
835
|
+
};
|
|
836
|
+
|
|
791
837
|
R.actions = (b, ctx) => {
|
|
792
838
|
const items = (b.items || []).map((it, i) => (typeof it === 'string' ? { label: it, command: it, _i: i } : { ...it, _i: i }));
|
|
793
839
|
const node = el(`
|
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.
|
|
@@ -15,8 +15,9 @@ import { saveAsRecipe } from './automations.js';
|
|
|
15
15
|
import { compareView } from './compare.js';
|
|
16
16
|
import { collapse, expand, handOff, renderFeed, setFocus } from './feed.js';
|
|
17
17
|
import { launchBehind } from './launcher.js';
|
|
18
|
+
import { copyTaskId, mentionBox } from './mentions.js';
|
|
18
19
|
import { runnerPicker } from './runner-picker.js';
|
|
19
|
-
import { agentName, effortsFor, modeMenu, MODES, paintRunnerBtn, savedEffort, savedModel } from './runners.js';
|
|
20
|
+
import { agentName, DEFAULT_MODE, effortsFor, modeMenu, MODES, paintRunnerBtn, savedEffort, savedModel } from './runners.js';
|
|
20
21
|
import { feedAnchor, keepInPlace } from './scroll.js';
|
|
21
22
|
import { mergePr, shipDialog, shipLabel, shippable } from './ship.js';
|
|
22
23
|
|
|
@@ -143,6 +144,7 @@ export function moreMenu(id, anchor) {
|
|
|
143
144
|
const menu = el(`
|
|
144
145
|
<div class="menu" style="top:${r.bottom + 6}px;right:${innerWidth - r.right}px">
|
|
145
146
|
<button data-a="rename">${icon('pen')}Rename${keyHint('M')}</button>
|
|
147
|
+
<button data-a="copy-id">${icon('copy')}Copy task ID</button>
|
|
146
148
|
${s.agentSessionId ? `<button data-a="copy">${icon('copy')}Copy resume command</button>` : ''}
|
|
147
149
|
<button data-a="folder">${icon('folder')}Open project in editor</button>
|
|
148
150
|
${canRollback ? `<button data-a="rollback">${icon(rolledBack ? 'rerun' : 'replay')}${rolledBack ? 'Re-apply latest turn' : 'Restore to before latest turn'}</button>` : ''}
|
|
@@ -153,6 +155,7 @@ export function moreMenu(id, anchor) {
|
|
|
153
155
|
const a = e.target.closest('button')?.dataset.a;
|
|
154
156
|
close();
|
|
155
157
|
if (a === 'rename') rename(id);
|
|
158
|
+
if (a === 'copy-id') copyTaskId(id);
|
|
156
159
|
if (a === 'copy') {
|
|
157
160
|
await tryApi(async () => {
|
|
158
161
|
const { command } = await api(`/sessions/${id}/takeover`);
|
|
@@ -178,6 +181,7 @@ export function taskMenu(id, e) {
|
|
|
178
181
|
<div class="menu task" style="top:${e.clientY}px;left:${e.clientX}px">
|
|
179
182
|
${S.open.has(id) ? `<button data-a="collapse">${icon('chevron')}Collapse${keyHint('O')}</button>` : `<button data-a="open">${icon('chevron')}Open${keyHint('O')}</button>`}
|
|
180
183
|
<button data-a="rename">${icon('pen')}Rename${keyHint('M')}</button>
|
|
184
|
+
<button data-a="copy-id">${icon('copy')}Copy task ID</button>
|
|
181
185
|
<button data-a="rerun">${icon('rerun')}Rerun…${keyHint('⇧R')}</button>
|
|
182
186
|
${shippable(s) ? `<button data-a="ship">${icon('send')}${shipLabel(s)}${keyHint('S')}</button>` : ''}
|
|
183
187
|
${s.pr?.state === 'OPEN' ? `<button data-a="merge">${icon('check')}Merge PR #${s.pr.number}…</button>` : ''}
|
|
@@ -193,6 +197,7 @@ export function taskMenu(id, e) {
|
|
|
193
197
|
if (a === 'open') expand(id);
|
|
194
198
|
if (a === 'collapse') collapse(id);
|
|
195
199
|
if (a === 'rename') rename(id);
|
|
200
|
+
if (a === 'copy-id') copyTaskId(id);
|
|
196
201
|
if (a === 'rerun') rerun(id);
|
|
197
202
|
if (a === 'ship') shipDialog(id);
|
|
198
203
|
if (a === 'merge') mergePr(id);
|
|
@@ -229,7 +234,7 @@ export async function rerun(id) {
|
|
|
229
234
|
const models = S.agents.find((x) => x.id === agent)?.models || [];
|
|
230
235
|
let model = agent === s.agent && models.some((m) => m.id === s.model) ? s.model : savedModel(agent);
|
|
231
236
|
let effort = agent === s.agent && effortsFor(agent, model).includes(s.effort) ? s.effort : savedEffort(agent, model);
|
|
232
|
-
let mode = MODES.some((m) => m.id === s.mode) ? s.mode : S.config.defaults.mode ||
|
|
237
|
+
let mode = MODES.some((m) => m.id === s.mode) ? s.mode : S.config.defaults.mode || DEFAULT_MODE.id;
|
|
233
238
|
const files = first.attachments || [];
|
|
234
239
|
|
|
235
240
|
const node = el(`
|
|
@@ -249,12 +254,12 @@ export async function rerun(id) {
|
|
|
249
254
|
</div>`);
|
|
250
255
|
const close = overlay(node);
|
|
251
256
|
$('.close', node).onclick = close;
|
|
252
|
-
const ta = $('.rr-prompt', node);
|
|
257
|
+
const ta = mentionBox($('.rr-prompt', node));
|
|
253
258
|
ta.value = first.prompt;
|
|
254
259
|
autosize(ta);
|
|
255
260
|
const modeBtn = $('[data-a="mode"]', node);
|
|
256
261
|
const paintMode = () => {
|
|
257
|
-
const m = MODES.find((x) => x.id === mode) ||
|
|
262
|
+
const m = MODES.find((x) => x.id === mode) || DEFAULT_MODE;
|
|
258
263
|
modeBtn.className = `mode-btn m-${m.id}`;
|
|
259
264
|
modeBtn.innerHTML = `${icon(m.icon)}<span>${m.label}</span>`;
|
|
260
265
|
modeBtn.title = m.hint;
|
|
@@ -178,7 +178,7 @@ export function openAutomations({ tab = 'recipes', edit = null, host } = {}) {
|
|
|
178
178
|
<div class="k-gh"><label><span>Check every</span><select name="every">${[2, 5, 15, 60].map((m) => `<option value="${m}" ${Number(d.every || 5) === m ? 'selected' : ''}>${m} min</option>`).join('')}</select></label></div>
|
|
179
179
|
<label><span>Run</span><select name="recipeId"><option value="">This prompt</option>${recipes.map((r) => `<option value="${r.id}" ${r.id === d.recipeId ? 'selected' : ''}>Recipe: ${esc(r.name)}</option>`).join('')}</select></label>
|
|
180
180
|
<label class="stack k-prompt"><span>Prompt <small class="muted k-vars"></small></span><textarea class="textarea mono" name="prompt" rows="6" spellcheck="false"></textarea></label>
|
|
181
|
-
<label><span>Mode</span><select name="mode">${modeOptions(d.mode || (d.kind === 'issues' ? 'plan' : '
|
|
181
|
+
<label><span>Mode</span><select name="mode">${modeOptions(d.mode || (d.kind === 'issues' ? 'plan' : 'auto'))}</select></label>
|
|
182
182
|
<div class="au-row"><span>Harness</span></div>
|
|
183
183
|
<p class="au-warn" hidden>${icon('warn')}<span>Issue text comes from GitHub: anyone who can open and label issues can steer this agent. Plan mode only reads; Full auto runs shell commands without asking.</span></p>
|
|
184
184
|
<div class="au-foot"><button type="button" class="ghost sm" data-act="cancel">Cancel</button><button class="primary sm">${d.id ? 'Save' : 'Create trigger'}</button></div>
|