wisp-agents 0.4.0 → 0.5.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 +8 -1
- package/README.md +15 -0
- package/package.json +1 -1
- package/server/index.js +37 -2
- package/server/runner.js +2 -0
- package/server/telemetry.js +132 -0
- package/web/js/app.js +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Anonymous usage stats and error reports via [PostHog](https://posthog.com): which agents, models and modes are used, run outcomes and timings, and errors (agent failures, server errors, crashes) with paths scrubbed. Never sends prompts, output, paths or project names. Turn it off in Settings → *Anonymous usage stats*, or with `WISP_TELEMETRY=0` / `DO_NOT_TRACK=1`; always off when `CI` is set. See the Telemetry section of the README.
|
|
13
|
+
|
|
8
14
|
## [0.4.0] - 2026-10-17
|
|
9
15
|
|
|
10
16
|
### Added
|
|
@@ -83,7 +89,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
83
89
|
- Session queue with follow-ups, answers to agent questions, terminal takeover and mark-as-done.
|
|
84
90
|
- Older turns are folded, with a floating collapse-all button and back-to-top.
|
|
85
91
|
|
|
86
|
-
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.
|
|
92
|
+
[Unreleased]: https://github.com/M0xoo/wisp/compare/v0.5.0...HEAD
|
|
93
|
+
[0.5.0]: https://github.com/M0xoo/wisp/compare/v0.4.0...v0.5.0
|
|
87
94
|
[0.4.0]: https://github.com/M0xoo/wisp/compare/v0.3.0...v0.4.0
|
|
88
95
|
[0.3.0]: https://github.com/M0xoo/wisp/compare/v0.2.2...v0.3.0
|
|
89
96
|
[0.2.2]: https://github.com/M0xoo/wisp/compare/v0.2.1...v0.2.2
|
package/README.md
CHANGED
|
@@ -202,6 +202,21 @@ If the harness has no system-prompt flag, prepend `systemPrompt` to the first pr
|
|
|
202
202
|
- [x] opencode adapter
|
|
203
203
|
- [x] Cursor CLI adapter
|
|
204
204
|
|
|
205
|
+
## Telemetry
|
|
206
|
+
|
|
207
|
+
wisp sends anonymous usage stats and error reports to [PostHog](https://posthog.com) so we can see which agents people use and what breaks. The code is in [`server/telemetry.js`](server/telemetry.js).
|
|
208
|
+
|
|
209
|
+
**What is sent:** a random install id (not tied to you or your machine), the wisp, OS, Node and agent CLI versions, and per run: agent, model, mode, effort, status, duration, token counts and cost, how many files changed, and which presentation block types came back. Errors include a stack trace and message with your home directory and file paths removed, cut to 500 characters.
|
|
210
|
+
|
|
211
|
+
**Never sent:** prompts, replies, agent output, file contents, project names, paths or repo URLs.
|
|
212
|
+
|
|
213
|
+
**Opt out** with any of these:
|
|
214
|
+
- Settings → *Anonymous usage stats*
|
|
215
|
+
- `WISP_TELEMETRY=0`
|
|
216
|
+
- `DO_NOT_TRACK=1`
|
|
217
|
+
|
|
218
|
+
It is also off whenever `CI` is set.
|
|
219
|
+
|
|
205
220
|
## Contributing
|
|
206
221
|
|
|
207
222
|
Issues and PRs are welcome. wisp has no build step: edit `web/` or `server/`, then run `npm run dev` (or `npm run dev:web`) to see your changes.
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { Runner, summarize, newId } from './runner.js';
|
|
|
11
11
|
import { adapters, listAgents } from './adapters/index.js';
|
|
12
12
|
import { welcomeSession } from './welcome.js';
|
|
13
13
|
import { openInTerminal, TERMINALS, shq } from './terminal.js';
|
|
14
|
+
import { Telemetry, envDisabled } from './telemetry.js';
|
|
14
15
|
|
|
15
16
|
const WEB = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'web');
|
|
16
17
|
|
|
@@ -62,6 +63,29 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
62
63
|
runner.on('finished', (f) => broadcast('finished', f));
|
|
63
64
|
setInterval(() => broadcast('ping', {}), 25_000).unref();
|
|
64
65
|
|
|
66
|
+
const telemetry = new Telemetry(store);
|
|
67
|
+
const run = (s, t) => ({ agent: s.agent, model: s.model || 'default', mode: s.mode, effort: s.effort || 'default', followUp: s.turns.indexOf(t) > 0 });
|
|
68
|
+
runner.on('started', ({ session, turn }) => telemetry.track('turn_started', { ...run(session, turn), attachments: turn.attachments.length }));
|
|
69
|
+
runner.on('ended', ({ session, turn, interrupted, errorText }) => {
|
|
70
|
+
const blocks = turn.presentation?.blocks || [];
|
|
71
|
+
const st = turn.stats || {};
|
|
72
|
+
telemetry.track('turn_ended', {
|
|
73
|
+
...run(session, turn),
|
|
74
|
+
status: interrupted ? 'interrupted' : turn.status, // done | error | stopped | interrupted
|
|
75
|
+
heroStatus: blocks.find((b) => b.type === 'hero')?.status,
|
|
76
|
+
durationMs: turn.endedAt - turn.startedAt,
|
|
77
|
+
blockCount: blocks.length,
|
|
78
|
+
blockTypes: [...new Set(blocks.map((b) => b.type))],
|
|
79
|
+
presentationFallback: !!turn.presentation?.fallback, // agent ignored or broke the wisp protocol
|
|
80
|
+
changedFiles: turn.changes.length,
|
|
81
|
+
inputTokens: st.inputTokens,
|
|
82
|
+
outputTokens: st.outputTokens,
|
|
83
|
+
costUsd: st.costUsd,
|
|
84
|
+
});
|
|
85
|
+
if (errorText) telemetry.captureError(new Error(errorText), { type: 'AgentError', ...run(session, turn) });
|
|
86
|
+
});
|
|
87
|
+
if (!envDisabled()) process.on('uncaughtExceptionMonitor', (err) => telemetry.crashed(err));
|
|
88
|
+
|
|
65
89
|
const allowedHosts = new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
|
|
66
90
|
|
|
67
91
|
const server = http.createServer(async (req, res) => {
|
|
@@ -81,6 +105,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
81
105
|
return serveStatic(res, url.pathname);
|
|
82
106
|
} catch (err) {
|
|
83
107
|
console.error(err);
|
|
108
|
+
if (!err.status || err.status >= 500) telemetry.captureError(err, { route: `${req.method} ${url.pathname.replace(/\/[a-f0-9]{8,}(?=\/|$)/g, '/:id')}` });
|
|
84
109
|
send(res, err.status || 500, { error: err.message });
|
|
85
110
|
}
|
|
86
111
|
});
|
|
@@ -107,6 +132,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
107
132
|
const body = await readJson(req);
|
|
108
133
|
const s = store.get(body.session);
|
|
109
134
|
if (!s) return send(res, 404, { error: 'no such session' });
|
|
135
|
+
telemetry.track('ask_user', { agent: s.agent });
|
|
110
136
|
return send(res, 200, { id: runner.ask(s, body) });
|
|
111
137
|
}
|
|
112
138
|
if (id === 'ask' && action && m === 'GET') return send(res, 200, runner.pollAnswer(action));
|
|
@@ -148,7 +174,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
148
174
|
}
|
|
149
175
|
|
|
150
176
|
if (root === 'config') {
|
|
151
|
-
if (m === 'GET') return send(res, 200, { ...store.config, home: os.homedir(), cwd: process.cwd(), terminals: TERMINALS });
|
|
177
|
+
if (m === 'GET') return send(res, 200, { ...store.config, home: os.homedir(), cwd: process.cwd(), terminals: TERMINALS, telemetry: { available: !envDisabled(), enabled: telemetry.enabled } });
|
|
152
178
|
if (m === 'PATCH') {
|
|
153
179
|
const body = await readJson(req);
|
|
154
180
|
if (body.defaults) Object.assign(store.config.defaults, body.defaults);
|
|
@@ -297,6 +323,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
297
323
|
}
|
|
298
324
|
openInTerminal({ cwd: s.cwd, argv, terminal: store.config.defaults.terminal || 'auto', custom: store.config.defaults.terminalCommand });
|
|
299
325
|
s.takenOverAt = Date.now();
|
|
326
|
+
telemetry.track('takeover', { agent: s.agent, terminal: store.config.defaults.terminal || 'auto' });
|
|
300
327
|
store.save(s);
|
|
301
328
|
return send(res, 200, { command });
|
|
302
329
|
}
|
|
@@ -341,6 +368,7 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
341
368
|
const shutdown = () => {
|
|
342
369
|
for (const s of store.all()) runner.stop(s);
|
|
343
370
|
store.flush();
|
|
371
|
+
telemetry.persist();
|
|
344
372
|
};
|
|
345
373
|
if (handleSignals) {
|
|
346
374
|
const exit = () => (shutdown(), process.exit(0));
|
|
@@ -353,7 +381,14 @@ export async function startServer({ port = 4477, host = '127.0.0.1', handleSigna
|
|
|
353
381
|
server.listen(port, host, resolve);
|
|
354
382
|
});
|
|
355
383
|
|
|
356
|
-
|
|
384
|
+
if (telemetry.enabled) {
|
|
385
|
+
if (store.config.defaults.telemetryNotice !== 'seen') console.log('wisp sends anonymous usage stats and error reports (no prompts, paths or code). Opt out: Settings, or WISP_TELEMETRY=0 / DO_NOT_TRACK=1.');
|
|
386
|
+
listAgents()
|
|
387
|
+
.then((agents) => telemetry.track('app_started', { agentsInstalled: agents.filter((a) => a.available).map((a) => a.id), ...Object.fromEntries(agents.filter((a) => a.available).map((a) => [`${a.id}Version`, a.version])) }))
|
|
388
|
+
.catch(() => {});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { url: `http://localhost:${port}`, server, store, runner, shutdown, telemetry };
|
|
357
392
|
}
|
|
358
393
|
|
|
359
394
|
function send(res, status, body) {
|
package/server/runner.js
CHANGED
|
@@ -220,6 +220,7 @@ export class Runner extends EventEmitter {
|
|
|
220
220
|
session.unread = false;
|
|
221
221
|
delete session.archived;
|
|
222
222
|
this.#touch(session, { turn: turn.id });
|
|
223
|
+
this.emit('started', { session, turn });
|
|
223
224
|
|
|
224
225
|
let released = false;
|
|
225
226
|
let linger;
|
|
@@ -341,6 +342,7 @@ export class Runner extends EventEmitter {
|
|
|
341
342
|
session.unread = !interrupted;
|
|
342
343
|
this.store.save(session, { now: true });
|
|
343
344
|
this.#touch(session, { turn: turn.id });
|
|
345
|
+
this.emit('ended', { session, turn, interrupted, errorText: ev.isError && !ev.stopped ? ev.text : '' });
|
|
344
346
|
// The follow-up starts right away; don't notify for the run it replaced.
|
|
345
347
|
if (!interrupted) this.emit('finished', { session: summarize(session, this.store) });
|
|
346
348
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Anonymous usage analytics + error reports, sent to PostHog over its plain HTTP API (no SDK, to
|
|
2
|
+
// stay dependency-free). What goes out is fixed values only — agent, model, mode, status, timings,
|
|
3
|
+
// counts — never prompts, paths, project names or agent output. Error messages and stacks are
|
|
4
|
+
// scrubbed of the home directory and truncated.
|
|
5
|
+
//
|
|
6
|
+
// Off when any of: no API key, DO_NOT_TRACK=1, WISP_TELEMETRY=0, CI, or "telemetry": "off" in
|
|
7
|
+
// settings. See the Telemetry section of the README.
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
// PostHog project API key: write-only, safe to ship in the client.
|
|
16
|
+
const KEY = process.env.WISP_POSTHOG_KEY ?? 'phc_wowwZkbP8e4zYuwT67JtX6oxdPwkMWL5kKnrUmYYXhcD';
|
|
17
|
+
const HOST = (process.env.WISP_POSTHOG_HOST || 'https://us.i.posthog.com').replace(/\/$/, '');
|
|
18
|
+
const FLUSH_MS = 15_000;
|
|
19
|
+
const MAX_QUEUE = 50;
|
|
20
|
+
const TIMEOUT_MS = 3000;
|
|
21
|
+
|
|
22
|
+
const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
23
|
+
const truthy = (v) => !!v && !/^(0|false|no|off)$/i.test(v);
|
|
24
|
+
|
|
25
|
+
export function envDisabled(env = process.env) {
|
|
26
|
+
return !KEY || truthy(env.DO_NOT_TRACK) || /^(0|false|no|off)$/i.test(env.WISP_TELEMETRY || '') || truthy(env.CI);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
30
|
+
|
|
31
|
+
// Paths in messages and stacks would leak usernames and project names: wisp's own files keep
|
|
32
|
+
// their package-relative path, anything else is cut down to its file name.
|
|
33
|
+
export function scrub(text = '', { home = os.homedir(), root = ROOT } = {}) {
|
|
34
|
+
let s = String(text).split(root).join('<wisp>');
|
|
35
|
+
if (home) s = s.split(home).join('~');
|
|
36
|
+
// Any other absolute path keeps only its file name.
|
|
37
|
+
return s
|
|
38
|
+
.replace(/(?<![\w<>~.:/\\-])(?:~|[A-Za-z]:)?[/\\](?:[^\s'"`():/\\]+[/\\])+([^\s'"`():/\\]*)/g, (m, base) => `…/${base}`)
|
|
39
|
+
.slice(0, 500);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function frames(stack = '') {
|
|
43
|
+
return String(stack)
|
|
44
|
+
.split('\n')
|
|
45
|
+
.slice(1, 30)
|
|
46
|
+
.map((l) => l.trim().match(/^at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?$/))
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map(([, fn, file, line, col]) => ({ platform: 'custom', lang: 'javascript', function: fn || '?', filename: scrub(file.replace(/^file:\/\//, '')), lineno: +line, colno: +col, in_app: file.includes(ROOT) && !file.includes('node_modules') }))
|
|
49
|
+
.reverse(); // PostHog wants the innermost frame last
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class Telemetry {
|
|
53
|
+
constructor(store, { shell = process.versions.electron ? 'electron' : 'browser' } = {}) {
|
|
54
|
+
this.store = store;
|
|
55
|
+
this.queue = [];
|
|
56
|
+
this.pendingFile = path.join(store.home, 'telemetry-pending.json');
|
|
57
|
+
this.common = {
|
|
58
|
+
$lib: 'wisp',
|
|
59
|
+
$process_person_profile: false, // anonymous events: no person profiles
|
|
60
|
+
version: VERSION,
|
|
61
|
+
os: process.platform,
|
|
62
|
+
osRelease: os.release(),
|
|
63
|
+
arch: process.arch,
|
|
64
|
+
node: process.versions.node,
|
|
65
|
+
electron: process.versions.electron || undefined,
|
|
66
|
+
shell,
|
|
67
|
+
};
|
|
68
|
+
this.timer = setInterval(() => this.flush(), FLUSH_MS);
|
|
69
|
+
this.timer.unref();
|
|
70
|
+
this.#sendPending();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get enabled() {
|
|
74
|
+
return !envDisabled() && this.store.config.defaults.telemetry !== 'off';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
track(event, props = {}) {
|
|
78
|
+
if (!this.enabled) return;
|
|
79
|
+
if (!this.store.config.telemetryId) {
|
|
80
|
+
this.store.config.telemetryId = randomUUID(); // random per install, not tied to the machine or user
|
|
81
|
+
this.store.saveConfig();
|
|
82
|
+
}
|
|
83
|
+
this.queue.push({ event, timestamp: new Date().toISOString(), properties: { ...this.common, ...props, distinct_id: this.store.config.telemetryId } });
|
|
84
|
+
if (this.queue.length >= MAX_QUEUE) this.flush();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// PostHog error tracking format.
|
|
88
|
+
captureError(err, props = {}) {
|
|
89
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
90
|
+
this.track('$exception', {
|
|
91
|
+
...props,
|
|
92
|
+
$exception_list: [{ type: props.type || e.name || 'Error', value: scrub(e.message), mechanism: { handled: props.handled ?? true, synthetic: false }, stacktrace: { type: 'raw', frames: frames(e.stack) } }],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async flush() {
|
|
97
|
+
if (!this.queue.length) return;
|
|
98
|
+
const batch = this.queue.splice(0);
|
|
99
|
+
try {
|
|
100
|
+
await fetch(`${HOST}/batch/`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'content-type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({ api_key: KEY, batch }),
|
|
104
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
105
|
+
});
|
|
106
|
+
} catch {} // offline or blocked: drop it, telemetry must never get in the way
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Shutdown and crashes leave no time for a request: write what's queued to disk (sync) and
|
|
110
|
+
// send it on the next start.
|
|
111
|
+
persist() {
|
|
112
|
+
clearInterval(this.timer);
|
|
113
|
+
if (!this.queue.length) return;
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(this.pendingFile, JSON.stringify(this.queue.splice(0)));
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
crashed(err) {
|
|
120
|
+
if (!this.enabled) return;
|
|
121
|
+
this.captureError(err, { handled: false, type: err?.name || 'UncaughtException' });
|
|
122
|
+
this.persist();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#sendPending() {
|
|
126
|
+
try {
|
|
127
|
+
const pending = JSON.parse(fs.readFileSync(this.pendingFile, 'utf8'));
|
|
128
|
+
fs.rmSync(this.pendingFile, { force: true });
|
|
129
|
+
if (this.enabled && Array.isArray(pending)) this.queue.push(...pending.slice(-200));
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/web/js/app.js
CHANGED
|
@@ -1701,6 +1701,7 @@ function openSettings() {
|
|
|
1701
1701
|
<label class="custom-term" ${d.terminal === 'custom' ? '' : 'hidden'}><span>Terminal command</span>
|
|
1702
1702
|
<input name="terminalCommand" value="${esc(d.terminalCommand || '')}" placeholder="alacritty --working-directory {cwd} -e {cmd}" spellcheck="false"></label>
|
|
1703
1703
|
<div class="setting"><span>Desktop notifications</span><span class="notif-hint muted xs"></span><button class="switch" id="notif" role="switch"></button></div>
|
|
1704
|
+
<div class="setting"><span>Anonymous usage stats</span><span class="tele-hint muted xs"></span><button class="switch" id="tele" role="switch"></button></div>
|
|
1704
1705
|
<div class="agents">${S.agents
|
|
1705
1706
|
.map((a) => `<div class="agent-row"><span class="dot ${a.available ? 'ok' : 'bad'}"></span><b>${esc(a.name)}</b><span class="muted">${esc(a.available ? a.version : a.error || 'not found')}</span></div>`)
|
|
1706
1707
|
.join('')}<p class="muted xs">Pick the agent next to the model in the launcher. Others plug in via <code>server/adapters</code>.</p></div>
|
|
@@ -1733,6 +1734,28 @@ function openSettings() {
|
|
|
1733
1734
|
paint();
|
|
1734
1735
|
};
|
|
1735
1736
|
paint();
|
|
1737
|
+
// Agent, model, status, timings and scrubbed errors only; never prompts, paths or code.
|
|
1738
|
+
const tele = $('#tele', node);
|
|
1739
|
+
const paintTele = () => {
|
|
1740
|
+
const t = S.config.telemetry || {};
|
|
1741
|
+
tele.setAttribute('aria-checked', !!t.enabled);
|
|
1742
|
+
tele.disabled = !t.available;
|
|
1743
|
+
$('.tele-hint', node).textContent = !t.available ? 'Off (DO_NOT_TRACK, WISP_TELEMETRY=0 or CI)' : 'No prompts, paths or code';
|
|
1744
|
+
};
|
|
1745
|
+
tele.onclick = async () => {
|
|
1746
|
+
await api('/config', { method: 'PATCH', body: { defaults: { telemetry: S.config.telemetry?.enabled ? 'off' : 'on' } } });
|
|
1747
|
+
await loadConfig();
|
|
1748
|
+
paintTele();
|
|
1749
|
+
};
|
|
1750
|
+
paintTele();
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Say once that anonymous stats are on, with a way out.
|
|
1754
|
+
function telemetryNotice() {
|
|
1755
|
+
if (!S.config.telemetry?.enabled || S.config.defaults.telemetryNotice === 'seen') return;
|
|
1756
|
+
S.config.defaults.telemetryNotice = 'seen';
|
|
1757
|
+
api('/config', { method: 'PATCH', body: { defaults: { telemetryNotice: 'seen' } } }).catch(() => {});
|
|
1758
|
+
toast('wisp sends anonymous usage stats and error reports. No prompts, paths or code.', { timeout: 10_000, action: { label: 'Settings', run: openSettings } });
|
|
1736
1759
|
}
|
|
1737
1760
|
|
|
1738
1761
|
function shortcuts() {
|
|
@@ -2041,4 +2064,5 @@ window.addEventListener('focus', () => {
|
|
|
2041
2064
|
connect();
|
|
2042
2065
|
route();
|
|
2043
2066
|
$('#prompt')?.focus();
|
|
2067
|
+
telemetryNotice();
|
|
2044
2068
|
})();
|