wisp-agents 0.11.0 → 0.13.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 +37 -1
- package/README.md +14 -5
- package/bin/wisp.js +2 -2
- package/package.json +2 -2
- package/server/adapters/index.js +4 -1
- package/server/client.js +1 -1
- package/server/index.js +69 -9
- package/server/machines.js +18 -1
- package/server/mcp-wisp.js +3 -3
- package/server/openapi.js +20 -3
- package/server/remote.js +103 -17
- package/server/runner.js +14 -6
- package/server/telemetry.js +58 -7
- package/web/css/app.css +68 -9
- package/web/index.html +2 -0
- package/web/js/app.js +356 -41
package/server/runner.js
CHANGED
|
@@ -13,6 +13,7 @@ import { captureCheckpoint, freshCheckpoint, readGitFile } from './checkpoint.js
|
|
|
13
13
|
import { teamNote, copies, SUBAGENT_ASK } from './subagents.js';
|
|
14
14
|
|
|
15
15
|
const MAX_ACTIVITY = 400;
|
|
16
|
+
const MODES = new Set(['plan', 'edits', 'auto']);
|
|
16
17
|
const LINGER_MS = 3000; // how long a CLI may keep running after it reported its result
|
|
17
18
|
const ASK_SERVER = path.join(path.dirname(fileURLToPath(import.meta.url)), 'mcp-ask.js');
|
|
18
19
|
// The delegate tool lives on the same injected server, so harnesses name it like ask_user.
|
|
@@ -70,7 +71,7 @@ export function summarize(s, store) {
|
|
|
70
71
|
toShip: s.toShip || null, // { dirty, ahead, unpushed } of the worktree, null until checked
|
|
71
72
|
pr: s.pr ? { number: s.pr.number, url: s.pr.url, state: s.pr.state, draft: s.pr.draft, review: s.pr.review, checks: s.pr.checks } : null,
|
|
72
73
|
groupId: s.groupId || null,
|
|
73
|
-
origin: s.origin || null, // { kind: 'recipe' | 'trigger', id, name }
|
|
74
|
+
origin: s.origin || null, // { kind: 'recipe' | 'trigger' | 'telegram', id, name }
|
|
74
75
|
// The roster, for the stacked logos on the card: used = delegated to at least once, busy = working now.
|
|
75
76
|
team: (s.subagents || []).flatMap((m) =>
|
|
76
77
|
copies(m).map((name) => {
|
|
@@ -138,8 +139,11 @@ export class Runner extends EventEmitter {
|
|
|
138
139
|
return session;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
// mode: switch the permission mode (plan / edits / auto) from this follow-up on. A queued
|
|
143
|
+
// follow-up keeps its own, so the run before it isn't affected.
|
|
144
|
+
reply(session, text, attachments = [], { queue = false, mode } = {}) {
|
|
145
|
+
if (!MODES.has(mode) || mode === session.mode) mode = undefined;
|
|
146
|
+
if (session.ask && !mode) {
|
|
143
147
|
this.answer(session, text);
|
|
144
148
|
return { answered: true };
|
|
145
149
|
}
|
|
@@ -149,22 +153,23 @@ export class Runner extends EventEmitter {
|
|
|
149
153
|
// The run already produced its result and the CLI is only lingering on its way out:
|
|
150
154
|
// there's nothing to wait for, so let it go and start the follow-up now.
|
|
151
155
|
if (turn?.status !== 'running') {
|
|
152
|
-
session.queue.push({ id: newId(), text, attachments });
|
|
156
|
+
session.queue.push({ id: newId(), text, attachments, ...(mode && { mode }) });
|
|
153
157
|
handle.release();
|
|
154
158
|
return { queued: session.queue.length > 0 };
|
|
155
159
|
}
|
|
156
160
|
// Interrupting needs an agent session to resume; before the agent reports one, queue.
|
|
157
161
|
if (queue || !session.agentSessionId) {
|
|
158
|
-
session.queue.push({ id: newId(), text, attachments });
|
|
162
|
+
session.queue.push({ id: newId(), text, attachments, ...(mode && { mode }) });
|
|
159
163
|
this.#touch(session);
|
|
160
164
|
return { queued: true, ahead: session.queue.length - 1 };
|
|
161
165
|
}
|
|
162
166
|
// Ahead of anything queued, but after earlier interrupting follow-ups, so they keep their order.
|
|
163
167
|
const at = session.queue.findIndex((q) => !q.now);
|
|
164
|
-
session.queue.splice(at < 0 ? session.queue.length : at, 0, { id: newId(), text, attachments, now: true });
|
|
168
|
+
session.queue.splice(at < 0 ? session.queue.length : at, 0, { id: newId(), text, attachments, now: true, ...(mode && { mode }) });
|
|
165
169
|
this.#interrupt(session);
|
|
166
170
|
return { interrupted: true };
|
|
167
171
|
}
|
|
172
|
+
if (mode) session.mode = mode;
|
|
168
173
|
this.#startTurn(session, text, attachments);
|
|
169
174
|
return { queued: false };
|
|
170
175
|
}
|
|
@@ -252,6 +257,7 @@ export class Runner extends EventEmitter {
|
|
|
252
257
|
let child = this.store.get(lead.children?.[slot]);
|
|
253
258
|
if (child) {
|
|
254
259
|
child.cwd = workdir(lead); // follows the lead if its worktree moved
|
|
260
|
+
child.mode = lead.mode; // and its mode, if a follow-up switched it
|
|
255
261
|
this.#startTurn(child, prompt);
|
|
256
262
|
} else {
|
|
257
263
|
child = this.create({ prompt, cwd: workdir(lead), agent: m.agent, mode: lead.mode, model: m.model, effort: m.effort, parentId: lead.id, member: slot });
|
|
@@ -317,6 +323,7 @@ export class Runner extends EventEmitter {
|
|
|
317
323
|
agent: session.agent,
|
|
318
324
|
model: session.model,
|
|
319
325
|
effort: session.effort || '',
|
|
326
|
+
mode: session.mode,
|
|
320
327
|
attachments: attachments.map(({ id, name, mime, size }) => ({ id, name, mime, size })),
|
|
321
328
|
startedAt: Date.now(),
|
|
322
329
|
endedAt: null,
|
|
@@ -391,6 +398,7 @@ export class Runner extends EventEmitter {
|
|
|
391
398
|
this.interrupting.delete(session.id);
|
|
392
399
|
this.stopping.delete(session.id); // a stop that landed after the result has nothing left to stop
|
|
393
400
|
const next = session.queue.shift();
|
|
401
|
+
if (next?.mode) session.mode = next.mode;
|
|
394
402
|
if (next && this.store.get(session.id)) this.#startTurn(session, next.text, next.attachments);
|
|
395
403
|
else this.#touch(session);
|
|
396
404
|
};
|
package/server/telemetry.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Off when any of: no API key, DO_NOT_TRACK=1, WISP_TELEMETRY=0, CI, or "telemetry": "off" in
|
|
7
7
|
// settings. See the Telemetry section of the README.
|
|
8
|
+
//
|
|
9
|
+
// Feedback (thumbs on a result, /feedback, "Send feedback") is different: the user hands it to us
|
|
10
|
+
// on purpose, so it goes out even with usage stats off. Only WISP_FEEDBACK=0 or no key stops it.
|
|
11
|
+
// Offline, it waits in feedback-outbox.json and goes with the next feedback or the next start.
|
|
8
12
|
|
|
9
13
|
import fs from 'node:fs';
|
|
10
14
|
import os from 'node:os';
|
|
@@ -22,10 +26,16 @@ const TIMEOUT_MS = 3000;
|
|
|
22
26
|
const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
23
27
|
const truthy = (v) => !!v && !/^(0|false|no|off)$/i.test(v);
|
|
24
28
|
|
|
29
|
+
const falsy = (v) => /^(0|false|no|off)$/i.test(v || '');
|
|
30
|
+
|
|
25
31
|
export function envDisabled(env = process.env) {
|
|
26
|
-
return !KEY || truthy(env.DO_NOT_TRACK) ||
|
|
32
|
+
return !KEY || truthy(env.DO_NOT_TRACK) || falsy(env.WISP_TELEMETRY) || truthy(env.CI);
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
export const feedbackAvailable = (env = process.env) => !!KEY && !falsy(env.WISP_FEEDBACK);
|
|
36
|
+
const FEEDBACK_MAX = 4000;
|
|
37
|
+
const OUTBOX_MAX = 100;
|
|
38
|
+
|
|
29
39
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
30
40
|
|
|
31
41
|
// Paths in messages and stacks would leak usernames and project names: wisp's own files keep
|
|
@@ -50,10 +60,12 @@ function frames(stack = '') {
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export class Telemetry {
|
|
53
|
-
constructor(store, { shell = process.versions.electron ? 'electron' : 'browser' } = {}) {
|
|
63
|
+
constructor(store, { shell = process.versions.electron ? 'electron' : 'browser', post } = {}) {
|
|
54
64
|
this.store = store;
|
|
55
65
|
this.queue = [];
|
|
56
66
|
this.pendingFile = path.join(store.home, 'telemetry-pending.json');
|
|
67
|
+
this.outboxFile = path.join(store.home, 'feedback-outbox.json');
|
|
68
|
+
if (post) this.post = post;
|
|
57
69
|
this.common = {
|
|
58
70
|
$lib: 'wisp',
|
|
59
71
|
$process_person_profile: false, // anonymous events: no person profiles
|
|
@@ -68,6 +80,7 @@ export class Telemetry {
|
|
|
68
80
|
this.timer = setInterval(() => this.flush(), FLUSH_MS);
|
|
69
81
|
this.timer.unref();
|
|
70
82
|
this.#sendPending();
|
|
83
|
+
this.#sendOutbox();
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
get enabled() {
|
|
@@ -93,17 +106,49 @@ export class Telemetry {
|
|
|
93
106
|
});
|
|
94
107
|
}
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
// Thumbs on a turn ('turn_rated') or a written note ('feedback'). Resolves true once PostHog has
|
|
110
|
+
// it; false means it's kept in the outbox and retried.
|
|
111
|
+
async feedback(event, props = {}) {
|
|
112
|
+
if (!feedbackAvailable()) return false;
|
|
113
|
+
// With usage stats off, a fresh id per message so feedback can't be tied to anything else.
|
|
114
|
+
const id = (this.enabled && this.store.config.telemetryId) || randomUUID();
|
|
115
|
+
const text = props.text ? String(props.text).split(os.homedir()).join('~').slice(0, FEEDBACK_MAX) : undefined;
|
|
116
|
+
const ev = { event, timestamp: new Date().toISOString(), properties: { ...this.common, ...props, text, distinct_id: id } };
|
|
117
|
+
const batch = [...this.#outbox(), ev].slice(-OUTBOX_MAX);
|
|
118
|
+
const ok = await this.post(batch);
|
|
119
|
+
try {
|
|
120
|
+
if (ok) fs.rmSync(this.outboxFile, { force: true });
|
|
121
|
+
else fs.writeFileSync(this.outboxFile, JSON.stringify(batch));
|
|
122
|
+
} catch {}
|
|
123
|
+
return ok;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#outbox() {
|
|
127
|
+
try {
|
|
128
|
+
const list = JSON.parse(fs.readFileSync(this.outboxFile, 'utf8'));
|
|
129
|
+
return Array.isArray(list) ? list : [];
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async post(batch) {
|
|
99
136
|
try {
|
|
100
|
-
await fetch(`${HOST}/batch/`, {
|
|
137
|
+
const res = await fetch(`${HOST}/batch/`, {
|
|
101
138
|
method: 'POST',
|
|
102
139
|
headers: { 'content-type': 'application/json' },
|
|
103
140
|
body: JSON.stringify({ api_key: KEY, batch }),
|
|
104
141
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
105
142
|
});
|
|
106
|
-
|
|
143
|
+
return res.ok;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async flush() {
|
|
150
|
+
if (!this.queue.length) return;
|
|
151
|
+
await this.post(this.queue.splice(0)); // offline or blocked: drop it, telemetry must never get in the way
|
|
107
152
|
}
|
|
108
153
|
|
|
109
154
|
// Shutdown and crashes leave no time for a request: write what's queued to disk (sync) and
|
|
@@ -122,6 +167,12 @@ export class Telemetry {
|
|
|
122
167
|
this.persist();
|
|
123
168
|
}
|
|
124
169
|
|
|
170
|
+
#sendOutbox() {
|
|
171
|
+
const batch = this.#outbox();
|
|
172
|
+
if (!batch.length || !feedbackAvailable()) return;
|
|
173
|
+
this.post(batch).then((ok) => ok && fs.rmSync(this.outboxFile, { force: true }));
|
|
174
|
+
}
|
|
175
|
+
|
|
125
176
|
#sendPending() {
|
|
126
177
|
try {
|
|
127
178
|
const pending = JSON.parse(fs.readFileSync(this.pendingFile, 'utf8'));
|
package/web/css/app.css
CHANGED
|
@@ -305,7 +305,8 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
|
|
|
305
305
|
.l-bar { display: flex; align-items: center; gap: 8px; padding-top: 6px; }
|
|
306
306
|
.l-bar > :not(.runners) { flex: none; }
|
|
307
307
|
.l-bar .grow { flex: 1; }
|
|
308
|
-
/* harness pill(s): "vs" rides on the end of it;
|
|
308
|
+
/* harness pill(s): "vs" rides on the end of it; each harness added joins the same pill behind a
|
|
309
|
+
"vs" of its own (hover: ×, removes it), and the end button turns into a + for the next one */
|
|
309
310
|
.runners {
|
|
310
311
|
display: inline-flex; align-items: stretch; min-width: 0; height: 30px; border-radius: 9px;
|
|
311
312
|
border: 1px solid var(--line); background: var(--surface); transition: border-color 0.15s;
|
|
@@ -318,6 +319,12 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
|
|
|
318
319
|
.runners .rb-h { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
319
320
|
.runners .rb-m { flex: 0 100 auto; min-width: 0; }
|
|
320
321
|
.runners.cmp .rb-e, .l-bar:has(.runners.cmp) #wtBtn span { display: none; }
|
|
322
|
+
/* three or four harnesses: logos only (each button's tooltip names it), the bar's other labels fold too */
|
|
323
|
+
.runners.many .runner-btn { flex: none; padding-inline: 6px; }
|
|
324
|
+
.runners.many :is(.rb-h, .rb-m) { display: none; }
|
|
325
|
+
.runners.many .rb-logo { margin-right: 3px; }
|
|
326
|
+
.runners.many .runner-btn .chev { margin-left: 0; }
|
|
327
|
+
.l-bar:has(.runners.many) :is(.mode-btn span, .team-btn .tb-label) { display: none; }
|
|
321
328
|
/* two harnesses on a narrow bar: drop the model names, then the harness names (logos stay) */
|
|
322
329
|
@container launcher (max-width: 640px) { .runners.cmp .rb-m { display: none; } }
|
|
323
330
|
@container launcher (max-width: 520px) {
|
|
@@ -326,20 +333,29 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
|
|
|
326
333
|
.runners.cmp .runner-btn .chev { margin-left: 0; }
|
|
327
334
|
.runners.cmp .rb-logo { margin-right: 4px; }
|
|
328
335
|
}
|
|
336
|
+
@container launcher (max-width: 600px) {
|
|
337
|
+
.runners.many .runner-btn .chev { display: none; }
|
|
338
|
+
.runners.many .vs-del { width: 18px; }
|
|
339
|
+
}
|
|
329
340
|
.vs-btn {
|
|
330
341
|
position: relative; flex: none; display: grid; place-items: center; width: 30px;
|
|
331
342
|
font: 600 11px var(--mono); letter-spacing: 0.02em; color: var(--faint); transition: color 0.15s, background 0.15s;
|
|
332
343
|
}
|
|
333
|
-
.vs-btn
|
|
344
|
+
.vs-btn[hidden] { display: none; }
|
|
334
345
|
.vs-btn:hover { color: var(--accent-ink); }
|
|
335
346
|
.vs-btn .i { display: none; width: 13px; height: 13px; }
|
|
336
347
|
.vs-btn:focus-visible { outline: 0; border-radius: 6px; box-shadow: inset 0 0 0 1px var(--accent); }
|
|
348
|
+
.vs-add::before { content: ''; position: absolute; left: 0; top: 7px; bottom: 7px; width: 1px; background: var(--line); }
|
|
337
349
|
.runners.cmp { border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
|
|
338
|
-
.runners.cmp .vs-
|
|
339
|
-
.runners.cmp .vs-
|
|
340
|
-
.runners.cmp .vs-
|
|
341
|
-
.
|
|
342
|
-
.runners.
|
|
350
|
+
.runners.cmp .vs-add { width: 26px; }
|
|
351
|
+
.runners.cmp .vs-add span { display: none; }
|
|
352
|
+
.runners.cmp .vs-add .i { display: block; width: 12px; height: 12px; }
|
|
353
|
+
.vs-del { width: 24px; color: var(--accent-ink); }
|
|
354
|
+
.runners.many .vs-del { width: 20px; }
|
|
355
|
+
.vs-del:hover { color: var(--bad); }
|
|
356
|
+
.vs-del:not(.fresh):is(:hover, :focus-visible) span { display: none; }
|
|
357
|
+
.vs-del:not(.fresh):is(:hover, :focus-visible) .i { display: block; }
|
|
358
|
+
.vs-del.fresh { cursor: default; }
|
|
343
359
|
/* permission mode: icon-only on the default, labelled otherwise */
|
|
344
360
|
.mode-btn {
|
|
345
361
|
display: inline-flex; align-items: center; gap: 6px; height: 32px; min-width: 32px; padding: 0 9px; border-radius: 9px;
|
|
@@ -434,13 +450,13 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
|
|
|
434
450
|
}
|
|
435
451
|
|
|
436
452
|
/* ---------- feed ---------- */
|
|
437
|
-
.feed-head { display: flex; align-items: center; gap: 14px; min-height: 22px; margin: 26px
|
|
438
|
-
.fh-status { display: flex; gap: 14px; }
|
|
453
|
+
.feed-head { display: flex; align-items: center; gap: 14px; min-height: 22px; margin: 26px 0 10px; padding: 0 4px; font-size: 13px; }
|
|
439
454
|
.feed-head[hidden] + .feed { margin-top: 26px; }
|
|
440
455
|
.hs { display: inline-flex; align-items: center; gap: 7px; color: var(--ink-2); }
|
|
441
456
|
.hs i { width: 7px; height: 7px; border-radius: 50%; }
|
|
442
457
|
.hs.working i { background: var(--accent); animation: pulse 1.6s infinite; }
|
|
443
458
|
.hs.needs i { background: var(--warn); }
|
|
459
|
+
.hs.calm i { background: var(--good); }
|
|
444
460
|
|
|
445
461
|
.f-group { margin-bottom: 22px; }
|
|
446
462
|
.f-group h4 {
|
|
@@ -462,6 +478,11 @@ body:has(.launcher.docked:is(:focus-within, :has(.open))) .to-top { display: non
|
|
|
462
478
|
.toggle-done:hover { color: var(--ink-2); }
|
|
463
479
|
.toggle-done .chev { width: 12px; height: 12px; transition: transform 0.2s; }
|
|
464
480
|
.f-group.done:not(.collapsed) .chev { transform: rotate(90deg); }
|
|
481
|
+
/* Folded groups are just a row of headings: stack them tight instead of leaving a section gap each. */
|
|
482
|
+
.f-group.collapsed { margin-bottom: 2px; }
|
|
483
|
+
.f-group.collapsed h4 { margin: 0; }
|
|
484
|
+
.f-group h4.toggle-done { width: fit-content; min-height: 24px; margin-left: -4px; padding: 0 8px; border-radius: 6px; transition: background 0.15s, color 0.15s; }
|
|
485
|
+
.f-group h4.toggle-done:hover { background: var(--bg-2); }
|
|
465
486
|
.f-list { display: flex; flex-direction: column; gap: 6px; }
|
|
466
487
|
.feed-empty { padding: 48px 0; text-align: center; color: var(--faint); font-size: 13.5px; }
|
|
467
488
|
|
|
@@ -1065,6 +1086,28 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
1065
1086
|
.trail-bar .link { font-size: 12px; }
|
|
1066
1087
|
.trail-body { padding: 8px 0 0; animation: rise 0.4s var(--ease); }
|
|
1067
1088
|
.trail-body .presentation { gap: 10px; }
|
|
1089
|
+
/* Thumbs: faint until the trail is hovered or the turn is rated, so a finished turn reads the same. */
|
|
1090
|
+
.rate { display: inline-flex; gap: 1px; margin-left: -6px; opacity: 0.5; transition: opacity 0.15s; }
|
|
1091
|
+
.turn:hover .rate, .rate.rated, .rate:focus-within { opacity: 1; }
|
|
1092
|
+
.rate-btn { display: inline-grid; place-items: center; width: 24px; height: 22px; border-radius: 6px; color: var(--faint); transition: color 0.15s, background 0.15s; }
|
|
1093
|
+
.rate-btn .i { width: 13px; height: 13px; }
|
|
1094
|
+
.rate-btn:hover { color: var(--ink); background: var(--bg-2); }
|
|
1095
|
+
.rate-btn[data-rate="up"].on { color: var(--good); }
|
|
1096
|
+
.rate-btn[data-rate="down"].on { color: var(--bad); }
|
|
1097
|
+
.rate-btn.on .i { fill: color-mix(in srgb, currentColor 18%, transparent); }
|
|
1098
|
+
.rate.thanked [data-rate="up"].on .i { animation: thumb-pop 0.45s var(--ease); }
|
|
1099
|
+
@keyframes thumb-pop { 40% { transform: scale(1.35) rotate(-12deg); } }
|
|
1100
|
+
.why { display: grid; gap: 6px; }
|
|
1101
|
+
.why .textarea { min-height: 0; resize: none; }
|
|
1102
|
+
.why-foot { display: flex; align-items: center; gap: 10px; }
|
|
1103
|
+
.why-foot .muted { margin-right: auto; }
|
|
1104
|
+
.why-foot .primary { height: 24px; padding: 0 10px; font-size: 12px; border-radius: 7px; }
|
|
1105
|
+
.fb-modal { width: min(520px, 100%); }
|
|
1106
|
+
.fb-rate { display: flex; align-items: center; gap: 4px; margin-bottom: 10px; font-size: 13px; color: var(--muted); }
|
|
1107
|
+
.fb-rate span { margin-right: 6px; }
|
|
1108
|
+
.fb-rate .rate-btn { width: 30px; height: 28px; }
|
|
1109
|
+
.fb-rate .rate-btn .i { width: 16px; height: 16px; }
|
|
1110
|
+
.fb-modal .ship-foot { margin-top: 12px; }
|
|
1068
1111
|
|
|
1069
1112
|
/* ---------- per-item composer ---------- */
|
|
1070
1113
|
.composer {
|
|
@@ -1082,6 +1125,12 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
1082
1125
|
.cq .att-x:hover { color: var(--bad); }
|
|
1083
1126
|
.cq-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
|
|
1084
1127
|
.c-hint { flex: 1; font-size: 11.5px; color: var(--faint); }
|
|
1128
|
+
.c-bar .mode-btn { height: 30px; min-width: 30px; padding: 0 8px; }
|
|
1129
|
+
.c-bar .mode-btn.changed { background: var(--bg-2); }
|
|
1130
|
+
.t-mode { display: inline-flex; align-items: center; gap: 4px; padding: 1px 6px; border-radius: 5px; font: 500 11px var(--sans); color: var(--muted); border: 1px solid var(--line); }
|
|
1131
|
+
.t-mode .i { width: 12px; height: 12px; }
|
|
1132
|
+
.t-mode.m-plan { color: var(--accent-ink); }
|
|
1133
|
+
.t-mode.m-auto { color: var(--warn); }
|
|
1085
1134
|
.send { display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px; background: var(--ink); color: var(--bg); transition: opacity 0.15s; }
|
|
1086
1135
|
.send:hover { opacity: 0.86; }
|
|
1087
1136
|
.send:disabled { opacity: 0.5; }
|
|
@@ -1200,6 +1249,8 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
1200
1249
|
.sx-ctl .input { width: 300px; }
|
|
1201
1250
|
.sx-state { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
1202
1251
|
.sx-state.bad { color: var(--bad); }
|
|
1252
|
+
#recheck.busy { cursor: progress; }
|
|
1253
|
+
#recheck.busy .i { animation: spin 0.8s linear infinite reverse; }
|
|
1203
1254
|
.sx-usage .it-loading { grid-column: 1 / -1; }
|
|
1204
1255
|
/* a view (Memory, Automations, Usage, Phone) opened inside Settings: no dialog chrome of its own */
|
|
1205
1256
|
.modal.embedded { width: auto; max-height: none; overflow: visible; padding: 0; border: 0; border-radius: 0; box-shadow: none; background: none; transform: none; }
|
|
@@ -1476,6 +1527,12 @@ pre.code { margin: 0 0 12px; padding: 12px 14px; border-radius: 10px; background
|
|
|
1476
1527
|
.mc-adv summary { cursor: pointer; width: max-content; font-size: 12.5px; color: var(--muted); }
|
|
1477
1528
|
.mc-adv[open] summary { margin-bottom: 8px; }
|
|
1478
1529
|
.mc-check { margin-top: 8px; }
|
|
1530
|
+
/* Hosted inside Settings: undo its row styling for labels (centred, padded, bordered, input flexed to 0 tall). */
|
|
1531
|
+
.mc-form label { padding: 0; border-bottom: 0; }
|
|
1532
|
+
.mc-form .mc-field { align-items: stretch; justify-content: flex-start; gap: 5px; }
|
|
1533
|
+
.mc-form .mc-field > span { text-align: left; }
|
|
1534
|
+
.mc-form .mc-field > .input { flex: none; max-width: none; }
|
|
1535
|
+
.mc-form .mc-check { display: inline-flex; justify-content: flex-start; gap: 8px; width: max-content; }
|
|
1479
1536
|
.mc-foot { display: flex; align-items: center; gap: 10px; }
|
|
1480
1537
|
.mc-foot .hint { flex: 1; margin: 0 !important; font-size: 12px !important; }
|
|
1481
1538
|
.mc-foot .primary { flex: none; }
|
|
@@ -1680,6 +1737,8 @@ body.electron.mac .topbar { padding-left: 88px; }
|
|
|
1680
1737
|
.ph-ok { color: var(--good); }
|
|
1681
1738
|
.ph-bad { color: var(--bad); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1682
1739
|
.ph-ok .i, .ph-bad .i { width: 12px; height: 12px; }
|
|
1740
|
+
.ph-start { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
|
|
1741
|
+
.ph-row { display: flex; align-items: center; gap: 8px; font-size: 13px; margin-bottom: 6px; }
|
|
1683
1742
|
|
|
1684
1743
|
/* ---------- subagents ---------- */
|
|
1685
1744
|
/* logo stack: the lead in front, its subagents tucked behind; they fan out on hover */
|
package/web/index.html
CHANGED
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 7.5V12l3 2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
|
57
57
|
<symbol id="i-laptop" viewBox="0 0 24 24"><rect x="4.5" y="5" width="15" height="10.5" rx="1.8" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M2.5 19h19" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
|
|
58
58
|
<symbol id="i-server" viewBox="0 0 24 24"><rect x="3.5" y="4" width="17" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><rect x="3.5" y="13" width="17" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><circle cx="7.3" cy="7.5" r="1.1" fill="currentColor"/><circle cx="7.3" cy="16.5" r="1.1" fill="currentColor"/><path d="M11 7.5h5.5M11 16.5h5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
|
|
59
|
+
<symbol id="i-thumb-up" viewBox="0 0 24 24"><path d="M7.5 10.5v9H4.8a.8.8 0 01-.8-.8v-7.4a.8.8 0 01.8-.8h2.7zm0 0l3.6-6.3a1.7 1.7 0 013.1 1.2l-.6 3.6h4.6a2 2 0 012 2.3l-1.1 6.4a2.4 2.4 0 01-2.4 2.1H7.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
|
|
60
|
+
<symbol id="i-thumb-down" viewBox="0 0 24 24"><path d="M7.5 13.5v-9H4.8a.8.8 0 00-.8.8v7.4a.8.8 0 00.8.8h2.7zm0 0l3.6 6.3a1.7 1.7 0 003.1-1.2l-.6-3.6h4.6a2 2 0 002-2.3l-1.1-6.4a2.4 2.4 0 00-2.4-2.1H7.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
|
|
59
61
|
<symbol id="i-play" viewBox="0 0 24 24"><path d="M7 5.5v13l11-6.5-11-6.5z" fill="currentColor"/></symbol>
|
|
60
62
|
</defs>
|
|
61
63
|
</svg>
|