clixad 0.0.1-beta.10 → 0.0.1-beta.12
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/dist/clixad.mjs +259 -25
- package/package.json +1 -1
package/dist/clixad.mjs
CHANGED
|
@@ -14,7 +14,83 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/edge.ts
|
|
18
|
+
function detectEdgeBlock(status, headers, body) {
|
|
19
|
+
if (status !== 403) return void 0;
|
|
20
|
+
if (isGatewayError(body)) return void 0;
|
|
21
|
+
const via = headers.get("server") ?? void 0;
|
|
22
|
+
const ray = rayId(headers, body);
|
|
23
|
+
return { status, ...ray ? { rayId: ray } : {}, ...via ? { via } : {} };
|
|
24
|
+
}
|
|
25
|
+
function isGatewayError(body) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(body);
|
|
28
|
+
return typeof parsed?.error === "string";
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function rayId(headers, body) {
|
|
34
|
+
const header = headers.get("cf-ray");
|
|
35
|
+
if (header) return header.split("-")[0];
|
|
36
|
+
const text = body.replace(/<[^>]*>/g, " ");
|
|
37
|
+
return /ray\s*id:?\s*([0-9a-f]{16})/i.exec(text)?.[1];
|
|
38
|
+
}
|
|
39
|
+
function edgeBlockSummary(block) {
|
|
40
|
+
const parts = [`${block.status} from ${block.via ?? "an intermediary"}`];
|
|
41
|
+
if (block.rayId) parts.push(`ray ${block.rayId}`);
|
|
42
|
+
return parts.join(", ");
|
|
43
|
+
}
|
|
44
|
+
function edgeBlockMessage(block, outcome) {
|
|
45
|
+
const head = `A security filter in front of the gateway rejected this request (${block.status}), so it
|
|
46
|
+
never reached Clixad. Nothing was sent to a model and nothing was charged.
|
|
47
|
+
Filters like that match on request contents, and a conversation about path
|
|
48
|
+
traversal, SQL or shell commands looks like an attack payload to one.`;
|
|
49
|
+
const tail = outcome === "retried" ? `
|
|
50
|
+
The compressed body this CLI falls back to was blocked as well.
|
|
51
|
+
Every turn resends the whole conversation, so the next one will be blocked
|
|
52
|
+
too \u2014 use /clear, or start a new session, to get past it.` : `
|
|
53
|
+
This gateway is too old to accept the compressed body that works around it.
|
|
54
|
+
Update the gateway, or use /clear to drop the history that trips the filter.`;
|
|
55
|
+
const id = block.rayId ? `
|
|
56
|
+
Request id (for support): ${block.rayId}` : "";
|
|
57
|
+
return head + tail + id;
|
|
58
|
+
}
|
|
59
|
+
function safeDetail(body, limit = DETAIL_LIMIT) {
|
|
60
|
+
if (!body) return "";
|
|
61
|
+
const title = /<title[^>]*>([^<]*)<\/title>/i.exec(body)?.[1];
|
|
62
|
+
const source = title ?? body;
|
|
63
|
+
const text = source.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)?/g, " ").replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, " ").replace(/<[^>]*>/g, " ").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
|
|
64
|
+
if (!text) return "";
|
|
65
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}\u2026` : text;
|
|
66
|
+
}
|
|
67
|
+
async function peek(res) {
|
|
68
|
+
const body = await res.text().catch(() => "");
|
|
69
|
+
return { body, res: new Response(body, { status: res.status, headers: res.headers }) };
|
|
70
|
+
}
|
|
71
|
+
var EdgeBlockedError, DETAIL_LIMIT;
|
|
72
|
+
var init_edge = __esm({
|
|
73
|
+
"src/edge.ts"() {
|
|
74
|
+
"use strict";
|
|
75
|
+
EdgeBlockedError = class extends Error {
|
|
76
|
+
constructor(block, outcome) {
|
|
77
|
+
super(edgeBlockMessage(block, outcome));
|
|
78
|
+
this.block = block;
|
|
79
|
+
this.outcome = outcome;
|
|
80
|
+
this.name = "EdgeBlockedError";
|
|
81
|
+
}
|
|
82
|
+
block;
|
|
83
|
+
outcome;
|
|
84
|
+
};
|
|
85
|
+
DETAIL_LIMIT = 200;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
17
89
|
// src/client.ts
|
|
90
|
+
import { gzipSync } from "node:zlib";
|
|
91
|
+
function encodeChatBody(json) {
|
|
92
|
+
return gzipSync(Buffer.from(json, "utf8"));
|
|
93
|
+
}
|
|
18
94
|
function adsPerTaskLabel(estAdsPerTask) {
|
|
19
95
|
const effort = estAdsPerTask >= 1 ? `~${estAdsPerTask} ads/task` : "<1 ad/task";
|
|
20
96
|
return `${effort} on completion`;
|
|
@@ -76,6 +152,15 @@ async function authFailure(res, what) {
|
|
|
76
152
|
`${what}: the gateway returned 401 without a Clixad error body. The gateway URL may be pointing at something else \u2014 a login/SSO page, a proxy, or a stale deployment. Check it, or set CLIXAD_GATEWAY_URL.`
|
|
77
153
|
);
|
|
78
154
|
}
|
|
155
|
+
async function triage(res) {
|
|
156
|
+
const peeked = await peek(res);
|
|
157
|
+
const block = detectEdgeBlock(res.status, res.headers, peeked.body);
|
|
158
|
+
return { ...block ? { block } : {}, res: peeked.res };
|
|
159
|
+
}
|
|
160
|
+
async function httpFailure(res, what) {
|
|
161
|
+
const detail = safeDetail(await res.text().catch(() => ""));
|
|
162
|
+
return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
|
|
163
|
+
}
|
|
79
164
|
function accumulateToolCall(calls, part, fallbackIndex) {
|
|
80
165
|
const index = part.index ?? fallbackIndex;
|
|
81
166
|
const current = calls.get(index) ?? { id: "", type: "function", function: { name: "", arguments: "" } };
|
|
@@ -102,14 +187,17 @@ async function* parseSSE(body) {
|
|
|
102
187
|
}
|
|
103
188
|
}
|
|
104
189
|
}
|
|
105
|
-
var LEDGER_REASONS, SignupClosedError, PaywallError, AuthError, GatewayClient;
|
|
190
|
+
var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, AuthError, GatewayClient;
|
|
106
191
|
var init_client = __esm({
|
|
107
192
|
"src/client.ts"() {
|
|
108
193
|
"use strict";
|
|
194
|
+
init_edge();
|
|
195
|
+
CHAT_ENCODED_CONTENT_TYPE = "application/vnd.clixad.chat+gzip";
|
|
109
196
|
LEDGER_REASONS = {
|
|
110
197
|
signup_bonus: "signup bonus",
|
|
111
198
|
ad_reward: "offer reward",
|
|
112
199
|
ad_screenout_bonus: "screenout bonus (didn't qualify)",
|
|
200
|
+
ad_sponsor_impression: "sponsored line shown",
|
|
113
201
|
ad_reversal: "offer reward reversed by the provider",
|
|
114
202
|
purchase: "credits purchased",
|
|
115
203
|
usage: "model usage",
|
|
@@ -148,6 +236,25 @@ var init_client = __esm({
|
|
|
148
236
|
this.config = config;
|
|
149
237
|
}
|
|
150
238
|
config;
|
|
239
|
+
/**
|
|
240
|
+
* Told when the client had to change how it talks to the gateway.
|
|
241
|
+
*
|
|
242
|
+
* Set by the REPL, unset everywhere else. A transport that silently changes
|
|
243
|
+
* itself is a thing the user should be able to see — it explains the pause,
|
|
244
|
+
* and it is the difference between "the tool is being slow" and "something in
|
|
245
|
+
* front of the gateway rejected that request". Left unset for `clixad -p` and
|
|
246
|
+
* the one-shot commands, whose stdout exists to be piped.
|
|
247
|
+
*/
|
|
248
|
+
onNotice;
|
|
249
|
+
/**
|
|
250
|
+
* How chat bodies are being sent, for the rest of this process.
|
|
251
|
+
*
|
|
252
|
+
* Sticky once it moves. The conversation is resent in full every turn, so the
|
|
253
|
+
* string that got the first request blocked is in every request after it —
|
|
254
|
+
* going back to plain JSON would mean paying a blocked round-trip per turn to
|
|
255
|
+
* rediscover the same thing.
|
|
256
|
+
*/
|
|
257
|
+
chatTransport = "json";
|
|
151
258
|
headers(auth = true) {
|
|
152
259
|
const h = { "Content-Type": "application/json" };
|
|
153
260
|
if (auth) {
|
|
@@ -179,7 +286,7 @@ var init_client = __esm({
|
|
|
179
286
|
headers: this.headers(false)
|
|
180
287
|
});
|
|
181
288
|
if (res.status === 501) return null;
|
|
182
|
-
if (!res.ok) throw new Error(
|
|
289
|
+
if (!res.ok) throw new Error(await httpFailure(res, "device start"));
|
|
183
290
|
return res.json();
|
|
184
291
|
}
|
|
185
292
|
/** Poll once for device-flow completion. `invite` unlocks a gated signup. */
|
|
@@ -229,7 +336,7 @@ var init_client = __esm({
|
|
|
229
336
|
});
|
|
230
337
|
if (res.status === 401) throw await authFailure(res, "checkout");
|
|
231
338
|
if (res.status === 501) throw new Error("Buying credits isn't enabled (Stripe not configured on the gateway).");
|
|
232
|
-
if (!res.ok) throw new Error(
|
|
339
|
+
if (!res.ok) throw new Error(await httpFailure(res, "checkout"));
|
|
233
340
|
return res.json();
|
|
234
341
|
}
|
|
235
342
|
// Both take an optional signal for the REPL's sake: they are awaited while the
|
|
@@ -263,6 +370,40 @@ var init_client = __esm({
|
|
|
263
370
|
if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
|
|
264
371
|
return res.json();
|
|
265
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Ask the gateway for a sponsored line, or `null` if it has none.
|
|
375
|
+
*
|
|
376
|
+
* **This never throws and never reports a failure to anybody.** Every caller is
|
|
377
|
+
* painting a row of dim chrome that already has a house line in it, so there is
|
|
378
|
+
* no state a user could act on and nothing a message would improve — a 404 from
|
|
379
|
+
* a gateway older than the route, a 401, a cold start, a dropped connection and
|
|
380
|
+
* an empty inventory are all the same event here, which is "keep showing ours".
|
|
381
|
+
* That is the opposite of every other method on this client, and it is why the
|
|
382
|
+
* catch is this broad rather than a list of statuses.
|
|
383
|
+
*
|
|
384
|
+
* The serve itself happens on the gateway, deliberately: an impression is worth
|
|
385
|
+
* credits, and this program is installed by strangers from npm. All the CLI
|
|
386
|
+
* gets to decide is when to ask.
|
|
387
|
+
*/
|
|
388
|
+
async sponsorLine(signal) {
|
|
389
|
+
try {
|
|
390
|
+
const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/line`, {
|
|
391
|
+
headers: this.headers(),
|
|
392
|
+
signal
|
|
393
|
+
});
|
|
394
|
+
if (!res.ok) return null;
|
|
395
|
+
const body = await res.json();
|
|
396
|
+
if (typeof body.text !== "string" || !body.text.trim()) return null;
|
|
397
|
+
return {
|
|
398
|
+
provider: "adtention",
|
|
399
|
+
id: typeof body.id === "string" && body.id ? body.id : "adtention",
|
|
400
|
+
text: body.text.trim(),
|
|
401
|
+
credits: typeof body.credits === "number" ? body.credits : 0
|
|
402
|
+
};
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
266
407
|
/**
|
|
267
408
|
* Search the web, through the gateway.
|
|
268
409
|
*
|
|
@@ -292,7 +433,14 @@ var init_client = __esm({
|
|
|
292
433
|
throw new Error(body.message ?? "Too many searches \u2014 wait a moment and try again.");
|
|
293
434
|
}
|
|
294
435
|
if (!res.ok) {
|
|
295
|
-
const
|
|
436
|
+
const peeked = await peek(res);
|
|
437
|
+
const block = detectEdgeBlock(res.status, res.headers, peeked.body);
|
|
438
|
+
if (block) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`The search was rejected by a security filter in front of the gateway (${edgeBlockSummary(block)}); the query reads as an attack payload to it. Rephrase it without the literal payload, or use web_fetch on a URL you know.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const body = await peeked.res.json().catch(() => ({}));
|
|
296
444
|
throw new Error(body.message ?? `web search failed: ${res.status}`);
|
|
297
445
|
}
|
|
298
446
|
const data = await res.json();
|
|
@@ -306,19 +454,70 @@ var init_client = __esm({
|
|
|
306
454
|
// wall, and the reward arrives as a signed postback from the network to the
|
|
307
455
|
// gateway. Nothing comes back to the CLI; a rising balance is the only signal,
|
|
308
456
|
// which is why both callers poll `wallet()`.
|
|
309
|
-
/**
|
|
310
|
-
|
|
311
|
-
const
|
|
457
|
+
/** One POST to the chat route, in whichever transport is currently in use. */
|
|
458
|
+
sendChat(payload, encoded, signal) {
|
|
459
|
+
const json = JSON.stringify(payload);
|
|
460
|
+
return fetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
|
|
312
461
|
method: "POST",
|
|
313
|
-
headers: this.headers(),
|
|
314
|
-
body:
|
|
462
|
+
headers: encoded ? { ...this.headers(), "Content-Type": CHAT_ENCODED_CONTENT_TYPE } : this.headers(),
|
|
463
|
+
body: encoded ? encodeChatBody(json) : json,
|
|
315
464
|
signal
|
|
316
465
|
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* POST a chat request, and get past the edge if the edge is in the way.
|
|
469
|
+
*
|
|
470
|
+
* Plain JSON first, always, on every fresh process. That is the OpenAI-compatible
|
|
471
|
+
* contract this route serves — Kimi CLI's kosong client speaks it, and so does
|
|
472
|
+
* anything else somebody points at `CLIXAD_GATEWAY_URL` — and a gateway that
|
|
473
|
+
* has never heard of the encoded form must keep working. The compressed body
|
|
474
|
+
* is a workaround for one specific failure, so it engages only once that
|
|
475
|
+
* failure has actually happened.
|
|
476
|
+
*
|
|
477
|
+
* The retry is not a general "try again": a 403 from a WAF is deterministic,
|
|
478
|
+
* and resending the identical bytes would be blocked identically. It is the
|
|
479
|
+
* *same* request in a form the filter has nothing to match on, which is why it
|
|
480
|
+
* is worth exactly one attempt and no more.
|
|
481
|
+
*
|
|
482
|
+
* Three outcomes, and each has to be distinguishable or the user is left
|
|
483
|
+
* guessing: the retry works (stick with it, say so once), the gateway answers
|
|
484
|
+
* 415 because it predates this (say that — the fix is a deploy, not a
|
|
485
|
+
* `/clear`), or it is blocked again (say that too, and say that clearing the
|
|
486
|
+
* history is the way out, because the history is what keeps triggering it).
|
|
487
|
+
*/
|
|
488
|
+
async postChat(payload, signal) {
|
|
489
|
+
const already = this.chatTransport === "encoded";
|
|
490
|
+
const first = await this.sendChat(payload, already, signal);
|
|
491
|
+
if (first.status !== 403) return first;
|
|
492
|
+
const { block, res } = await triage(first);
|
|
493
|
+
if (!block) return res;
|
|
494
|
+
if (already) throw new EdgeBlockedError(block, "retried");
|
|
495
|
+
const retry = await this.sendChat(payload, true, signal);
|
|
496
|
+
if (retry.status === 415) throw new EdgeBlockedError(block, "unsupported");
|
|
497
|
+
if (retry.status === 403) {
|
|
498
|
+
const second = await triage(retry);
|
|
499
|
+
if (second.block) throw new EdgeBlockedError(second.block, "retried");
|
|
500
|
+
return second.res;
|
|
501
|
+
}
|
|
502
|
+
this.chatTransport = "encoded";
|
|
503
|
+
this.onNotice?.(
|
|
504
|
+
` A security filter in front of the gateway blocked that request (${edgeBlockSummary(block)}).
|
|
505
|
+
Resent with a compressed body, which got through. Nothing was lost, and the
|
|
506
|
+
rest of this session will use the same encoding.`
|
|
507
|
+
);
|
|
508
|
+
return retry;
|
|
509
|
+
}
|
|
510
|
+
/** Non-streaming chat. Kept for scripted/one-shot paths; the REPL streams. */
|
|
511
|
+
async chat(messages, model, tools, signal) {
|
|
512
|
+
const res = await this.postChat(
|
|
513
|
+
{ model, messages, stream: false, ...tools ? { tools } : {} },
|
|
514
|
+
signal
|
|
515
|
+
);
|
|
317
516
|
if (res.status === 401) throw await authFailure(res, "chat");
|
|
318
517
|
if (res.status === 402) {
|
|
319
518
|
throw paywallError(await res.json());
|
|
320
519
|
}
|
|
321
|
-
if (!res.ok) throw new Error(
|
|
520
|
+
if (!res.ok) throw new Error(await httpFailure(res, "chat"));
|
|
322
521
|
const data = await res.json();
|
|
323
522
|
const choice = data.choices[0];
|
|
324
523
|
if (!choice) throw new Error("chat: no choices in response");
|
|
@@ -336,17 +535,15 @@ var init_client = __esm({
|
|
|
336
535
|
* the agent loop can stream *and* use tools in the same request.
|
|
337
536
|
*/
|
|
338
537
|
async chatStream(messages, model, opts = {}) {
|
|
339
|
-
const res = await
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
signal: opts.signal
|
|
344
|
-
});
|
|
538
|
+
const res = await this.postChat(
|
|
539
|
+
{ model, messages, stream: true, ...opts.tools ? { tools: opts.tools } : {} },
|
|
540
|
+
opts.signal
|
|
541
|
+
);
|
|
345
542
|
if (res.status === 401) throw await authFailure(res, "chat");
|
|
346
543
|
if (res.status === 402) {
|
|
347
544
|
throw paywallError(await res.json());
|
|
348
545
|
}
|
|
349
|
-
if (!res.ok || !res.body) throw new Error(
|
|
546
|
+
if (!res.ok || !res.body) throw new Error(await httpFailure(res, "chat"));
|
|
350
547
|
let content = "";
|
|
351
548
|
let finishReason = "stop";
|
|
352
549
|
let creditsCharged = 0;
|
|
@@ -3840,6 +4037,23 @@ function sponsorSource(provider = "house") {
|
|
|
3840
4037
|
return houseSource();
|
|
3841
4038
|
}
|
|
3842
4039
|
}
|
|
4040
|
+
function adtentionSource(fetchLine) {
|
|
4041
|
+
return {
|
|
4042
|
+
provider: "adtention",
|
|
4043
|
+
async fetch(signal) {
|
|
4044
|
+
let line2;
|
|
4045
|
+
try {
|
|
4046
|
+
line2 = await fetchLine(signal);
|
|
4047
|
+
} catch {
|
|
4048
|
+
return null;
|
|
4049
|
+
}
|
|
4050
|
+
if (!line2) return null;
|
|
4051
|
+
const text = line2.text.trim();
|
|
4052
|
+
if (!text) return null;
|
|
4053
|
+
return { id: line2.id, text };
|
|
4054
|
+
}
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
3843
4057
|
var init_provider = __esm({
|
|
3844
4058
|
"src/sponsor/provider.ts"() {
|
|
3845
4059
|
"use strict";
|
|
@@ -4279,6 +4493,13 @@ function headRows(text, cols, max) {
|
|
|
4279
4493
|
}
|
|
4280
4494
|
return kept.join("\n");
|
|
4281
4495
|
}
|
|
4496
|
+
function errorNotice(message, rows = ERROR_NOTICE_ROWS) {
|
|
4497
|
+
const clean = message.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)?/g, " ").replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g, " ");
|
|
4498
|
+
const lines = clean.split("\n");
|
|
4499
|
+
const kept = lines.slice(0, rows).map((l) => l.length > ERROR_NOTICE_COLS ? `${l.slice(0, ERROR_NOTICE_COLS - 1)}\u2026` : l);
|
|
4500
|
+
if (lines.length > rows) kept.push(` \u2026 +${lines.length - rows} more lines`);
|
|
4501
|
+
return kept.join("\n");
|
|
4502
|
+
}
|
|
4282
4503
|
function entryHeight(entry, cols) {
|
|
4283
4504
|
const margin = entry.kind === "banner" ? 0 : 1;
|
|
4284
4505
|
switch (entry.kind) {
|
|
@@ -4325,7 +4546,7 @@ function EntryView({ entry }) {
|
|
|
4325
4546
|
) });
|
|
4326
4547
|
}
|
|
4327
4548
|
}
|
|
4328
|
-
var MANGO3, MANGO_BRIGHT, SLATE, MODE_STYLE;
|
|
4549
|
+
var MANGO3, MANGO_BRIGHT, SLATE, MODE_STYLE, ERROR_NOTICE_ROWS, ERROR_NOTICE_COLS;
|
|
4329
4550
|
var init_views = __esm({
|
|
4330
4551
|
"src/tui/views.tsx"() {
|
|
4331
4552
|
"use strict";
|
|
@@ -4339,6 +4560,8 @@ var init_views = __esm({
|
|
|
4339
4560
|
acceptEdits: { color: "#7ee787", glyph: "\u23F5\u23F5" },
|
|
4340
4561
|
plan: { color: "#79c0ff", glyph: "\u23F8" }
|
|
4341
4562
|
};
|
|
4563
|
+
ERROR_NOTICE_ROWS = 12;
|
|
4564
|
+
ERROR_NOTICE_COLS = 200;
|
|
4342
4565
|
}
|
|
4343
4566
|
});
|
|
4344
4567
|
|
|
@@ -4434,6 +4657,8 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
4434
4657
|
const sponsorRef = useRef(sponsorSource());
|
|
4435
4658
|
const sponsorIdxRef = useRef(0);
|
|
4436
4659
|
const sponsorPrevRef = useRef(void 0);
|
|
4660
|
+
const sponsorCallRef = useRef(0);
|
|
4661
|
+
const remoteSponsorRef = useRef(adtentionSource((signal) => client.sponsorLine(signal)));
|
|
4437
4662
|
const tallyRef = useRef(createTally());
|
|
4438
4663
|
const heightCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
4439
4664
|
const push = useCallback((e) => {
|
|
@@ -4473,6 +4698,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
4473
4698
|
}, []);
|
|
4474
4699
|
useEffect(() => dropDelta, [dropDelta]);
|
|
4475
4700
|
useEffect(() => {
|
|
4701
|
+
client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
|
|
4476
4702
|
const files = contextRef.current.files;
|
|
4477
4703
|
if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
|
|
4478
4704
|
if (session?.messages.length) {
|
|
@@ -4520,6 +4746,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
4520
4746
|
);
|
|
4521
4747
|
const nextSponsor = useCallback(
|
|
4522
4748
|
(modelId) => {
|
|
4749
|
+
const call = ++sponsorCallRef.current;
|
|
4523
4750
|
if (!sponsorEnabled({
|
|
4524
4751
|
isTTY: process.stdout.isTTY,
|
|
4525
4752
|
env: process.env,
|
|
@@ -4534,13 +4761,19 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
4534
4761
|
adsPerTask: est === void 0 ? void 0 : adsPerTaskLabel(est)
|
|
4535
4762
|
};
|
|
4536
4763
|
const picked = sponsorRef.current.pick(sponsorIdxRef.current++, sponsorPrevRef.current, facts);
|
|
4537
|
-
if (
|
|
4764
|
+
if (picked) {
|
|
4765
|
+
sponsorPrevRef.current = picked.id;
|
|
4766
|
+
recordImpression(tallyRef.current, picked.id);
|
|
4767
|
+
setSponsor(picked.text);
|
|
4768
|
+
} else {
|
|
4538
4769
|
setSponsor(null);
|
|
4539
|
-
return;
|
|
4540
4770
|
}
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4771
|
+
void remoteSponsorRef.current.fetch().then((line2) => {
|
|
4772
|
+
if (!line2 || call !== sponsorCallRef.current) return;
|
|
4773
|
+
sponsorPrevRef.current = line2.id;
|
|
4774
|
+
recordImpression(tallyRef.current, line2.id);
|
|
4775
|
+
setSponsor(line2.text);
|
|
4776
|
+
});
|
|
4544
4777
|
},
|
|
4545
4778
|
[config.sponsor, wallet]
|
|
4546
4779
|
);
|
|
@@ -4760,13 +4993,14 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
4760
4993
|
} else if (err instanceof AuthError) {
|
|
4761
4994
|
push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
|
|
4762
4995
|
} else {
|
|
4763
|
-
push({ kind: "notice", tone: "error", text: ` error: ${err.message}` });
|
|
4996
|
+
push({ kind: "notice", tone: "error", text: ` error: ${errorNotice(err.message)}` });
|
|
4764
4997
|
}
|
|
4765
4998
|
} finally {
|
|
4766
4999
|
abortRef.current = null;
|
|
4767
5000
|
dropDelta();
|
|
4768
5001
|
setLive(null);
|
|
4769
5002
|
setBusy(false);
|
|
5003
|
+
sponsorCallRef.current++;
|
|
4770
5004
|
setSponsor(null);
|
|
4771
5005
|
}
|
|
4772
5006
|
},
|
|
@@ -4801,7 +5035,7 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
4801
5035
|
} catch (err) {
|
|
4802
5036
|
if (ac.signal.aborted) push({ kind: "notice", tone: "warn", text: " (stopped)" });
|
|
4803
5037
|
else if (err instanceof AuthError) push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
|
|
4804
|
-
else push({ kind: "notice", tone: "error", text: ` ${err.message}` });
|
|
5038
|
+
else push({ kind: "notice", tone: "error", text: ` ${errorNotice(err.message)}` });
|
|
4805
5039
|
} finally {
|
|
4806
5040
|
busyAbortRef.current = null;
|
|
4807
5041
|
setBusyLabel(WORKING);
|