clixad 0.0.1-beta.21 → 0.0.1-beta.22

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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/dist/clixad.mjs +609 -130
  3. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -14,6 +14,94 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
+ // src/estimate.ts
18
+ function parseTaskShape(raw) {
19
+ if (!raw || typeof raw !== "object") return void 0;
20
+ const r = raw;
21
+ const count = (v, min) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= 1e6 ? Math.floor(v) : void 0;
22
+ const callsTypical = count(r.calls_typical, 1);
23
+ const callsLong = count(r.calls_long, 1);
24
+ const growthTokens = count(r.growth_tokens, 0);
25
+ const outputTokens = count(r.output_tokens, 0);
26
+ if (callsTypical === void 0 || callsLong === void 0) return void 0;
27
+ if (growthTokens === void 0 || outputTokens === void 0) return void 0;
28
+ if (callsLong < callsTypical) return void 0;
29
+ return { callsTypical, callsLong, growthTokens, outputTokens };
30
+ }
31
+ function callCredits(model, inputTokens, outputTokens) {
32
+ return Math.ceil(
33
+ (inputTokens * model.credits_per_mtoken_input + outputTokens * model.credits_per_mtoken_output) / 1e6
34
+ );
35
+ }
36
+ function taskCredits(model, contextTokens, calls, shape) {
37
+ const start = Math.max(0, Math.round(contextTokens));
38
+ let total = 0;
39
+ for (let i = 0; i < calls; i++) {
40
+ total += callCredits(model, start + i * shape.growthTokens, shape.outputTokens);
41
+ }
42
+ return total;
43
+ }
44
+ function estimateTask(model, contextTokens, shape) {
45
+ return {
46
+ low: taskCredits(model, contextTokens, shape.callsTypical, shape),
47
+ high: taskCredits(model, contextTokens, shape.callsLong, shape)
48
+ };
49
+ }
50
+ function taskVerdict(range, wallet) {
51
+ const free = Math.max(0, wallet.freeLeft ?? 0);
52
+ if (free > 0 && free >= range.high) return "free";
53
+ const available = Math.max(0, wallet.balance) + free;
54
+ if (available >= range.high) return "fits";
55
+ if (available >= range.low) return "tight";
56
+ return "short";
57
+ }
58
+ function shortCredits(n, direction) {
59
+ const round = direction === "down" ? Math.floor : Math.ceil;
60
+ if (n < 1e3) return String(Math.max(0, Math.round(n)));
61
+ if (n < 1e4) return `${trim(round(n / 100) / 10)}k`;
62
+ if (n < 1e6) return `${round(n / 1e3)}k`;
63
+ return `${trim(round(n / 1e5) / 10)}M`;
64
+ }
65
+ function trim(n) {
66
+ return Number.isInteger(n) ? String(n) : n.toFixed(1);
67
+ }
68
+ function estimateClause(range, verdict, freeElsewhere) {
69
+ const span = range.low === range.high ? shortCredits(range.high, "up") : `${shortCredits(range.low, "down")}\u2013${shortCredits(range.high, "up")}`;
70
+ const head = `task \u2248 ${span}`;
71
+ const tight = `\u2248${span}`;
72
+ switch (verdict) {
73
+ case "free":
74
+ return { variants: [`${head} \xB7 free`, `${tight} free`], warn: false };
75
+ case "fits":
76
+ return { variants: [`${head} cr`, `${tight} cr`], warn: false };
77
+ case "tight":
78
+ return { variants: [`${head} \xB7 may run short`, `${tight}, may run short`, `${tight} tight`], warn: true };
79
+ case "short":
80
+ return {
81
+ variants: [
82
+ `${head} \xB7 not enough, ${freeElsewhere ? "/model" : "/earn"}`,
83
+ `${tight}, not enough`,
84
+ `${tight} short`
85
+ ],
86
+ warn: true
87
+ };
88
+ }
89
+ }
90
+ var init_estimate = __esm({
91
+ "src/estimate.ts"() {
92
+ "use strict";
93
+ }
94
+ });
95
+
96
+ // src/sponsor/formats.ts
97
+ var EXTRA_AD_FORMATS;
98
+ var init_formats = __esm({
99
+ "src/sponsor/formats.ts"() {
100
+ "use strict";
101
+ EXTRA_AD_FORMATS = ["sidebar_card", "status_bar"];
102
+ }
103
+ });
104
+
17
105
  // src/edge.ts
18
106
  function detectEdgeBlock(status, headers, body) {
19
107
  if (status !== 403) return void 0;
@@ -225,6 +313,17 @@ function redeemMessage(result) {
225
313
  function ledgerReasonLabel(reason) {
226
314
  return LEDGER_REASONS[reason] ?? reason.replace(/_/g, " ");
227
315
  }
316
+ function extraFormatIds(raw) {
317
+ const out = {};
318
+ if (typeof raw !== "object" || raw === null) return out;
319
+ for (const format of EXTRA_AD_FORMATS) {
320
+ const entry = raw[format];
321
+ if (typeof entry !== "object" || entry === null) continue;
322
+ const id = entry.publisher_id;
323
+ if (typeof id === "string" && id.trim()) out[format] = id.trim();
324
+ }
325
+ return out;
326
+ }
228
327
  function imagePart(image) {
229
328
  return { type: "image_url", image_url: { url: `data:${image.mimeType};base64,${image.data}` } };
230
329
  }
@@ -252,7 +351,10 @@ function asSignupClosed(status, data) {
252
351
  return d && d.error === "signup_closed" && typeof d.message === "string" ? { error: d.error, message: d.message, waitlist_url: d.waitlist_url ?? "https://clixad.io" } : void 0;
253
352
  }
254
353
  function paywallError(body) {
255
- return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
354
+ const free = Array.isArray(body.free_models) ? body.free_models.filter(
355
+ (m) => !!m && typeof m.id === "string" && m.id.length > 0 && Number.isFinite(m.turns_left)
356
+ ).map((m) => ({ id: m.id, turnsLeft: Math.max(0, Math.floor(m.turns_left)) })).filter((m) => m.turnsLeft > 0) : [];
357
+ return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad, free);
256
358
  }
257
359
  async function authFailure(res, what) {
258
360
  const body = await res.text().catch(() => "");
@@ -278,16 +380,16 @@ async function httpFailure(res, what) {
278
380
  return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
279
381
  }
280
382
  function sleep(ms, signal) {
281
- return new Promise((resolve2) => {
383
+ return new Promise((resolve3) => {
282
384
  if (!signal) {
283
- setTimeout(resolve2, ms);
385
+ setTimeout(resolve3, ms);
284
386
  return;
285
387
  }
286
- if (signal.aborted) return resolve2();
388
+ if (signal.aborted) return resolve3();
287
389
  const done = () => {
288
390
  clearTimeout(timer);
289
391
  signal.removeEventListener("abort", done);
290
- resolve2();
392
+ resolve3();
291
393
  };
292
394
  const timer = setTimeout(done, ms);
293
395
  signal.addEventListener("abort", done, { once: true });
@@ -331,6 +433,8 @@ var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError,
331
433
  var init_client = __esm({
332
434
  "src/client.ts"() {
333
435
  "use strict";
436
+ init_estimate();
437
+ init_formats();
334
438
  init_edge();
335
439
  CHAT_ENCODED_CONTENT_TYPE = "application/vnd.clixad.chat+gzip";
336
440
  LEDGER_REASONS = {
@@ -361,16 +465,18 @@ var init_client = __esm({
361
465
  waitlistUrl;
362
466
  };
363
467
  PaywallError = class extends Error {
364
- constructor(balance, estimatedCost, grantPerAd) {
468
+ constructor(balance, estimatedCost, grantPerAd, freeModels = []) {
365
469
  super("Out of credits");
366
470
  this.balance = balance;
367
471
  this.estimatedCost = estimatedCost;
368
472
  this.grantPerAd = grantPerAd;
473
+ this.freeModels = freeModels;
369
474
  this.name = "PaywallError";
370
475
  }
371
476
  balance;
372
477
  estimatedCost;
373
478
  grantPerAd;
479
+ freeModels;
374
480
  };
375
481
  AuthError = class extends Error {
376
482
  constructor() {
@@ -640,8 +746,18 @@ var init_client = __esm({
640
746
  if (!res.ok) throw new Error(`models failed: ${res.status}`);
641
747
  const data = await res.json();
642
748
  this.seen = { web_search: data.capabilities?.web_search === true };
749
+ this.shape = parseTaskShape(data.task_shape);
643
750
  return data.data;
644
751
  }
752
+ /**
753
+ * The task shape the cost preview assumes, as of the last catalog fetch, or
754
+ * undefined when the gateway sent none (or one that did not parse). Undefined
755
+ * means no estimate is shown at all, the silence an older gateway gets.
756
+ */
757
+ get taskShape() {
758
+ return this.shape;
759
+ }
760
+ shape;
645
761
  /**
646
762
  * What the gateway said it can do, as of the last catalog fetch.
647
763
  *
@@ -701,11 +817,13 @@ var init_client = __esm({
701
817
  const body = await res.json();
702
818
  const publisherId = typeof body.publisher_id === "string" ? body.publisher_id.trim() : "";
703
819
  if (!publisherId) return null;
820
+ const formats = extraFormatIds(body.formats);
704
821
  return {
705
822
  publisherId,
706
823
  ...typeof body.category === "string" ? { category: body.category } : {},
707
824
  ...typeof body.subject === "string" ? { subject: body.subject } : {},
708
- ...typeof body.dwell_ms === "number" ? { dwellMs: body.dwell_ms } : {}
825
+ ...typeof body.dwell_ms === "number" ? { dwellMs: body.dwell_ms } : {},
826
+ ...Object.keys(formats).length > 0 ? { formats } : {}
709
827
  };
710
828
  } catch {
711
829
  return null;
@@ -735,6 +853,7 @@ var init_client = __esm({
735
853
  ...signal ? { signal } : {},
736
854
  body: JSON.stringify({
737
855
  impression_id: report.impressionId,
856
+ ...report.format && report.format !== "top_bar" ? { format: report.format } : {},
738
857
  ...report.adId ? { ad_id: report.adId } : {},
739
858
  billable: report.billable,
740
859
  from_cache: report.fromCache,
@@ -1132,12 +1251,13 @@ function saveConfig(config) {
1132
1251
  function configPath() {
1133
1252
  return CONFIG_PATH;
1134
1253
  }
1135
- var CONFIG_PATH, QUARANTINE_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS, baseline, envApplied, notices;
1254
+ var CONFIG_PATH, QUARANTINE_PATH, FALLBACK_MODEL, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS, baseline, envApplied, notices;
1136
1255
  var init_config = __esm({
1137
1256
  "src/config.ts"() {
1138
1257
  "use strict";
1139
1258
  CONFIG_PATH = join(homedir(), ".clixad", "config.json");
1140
1259
  QUARANTINE_PATH = `${CONFIG_PATH}.corrupt`;
1260
+ FALLBACK_MODEL = "gemini-2.5-flash-lite";
1141
1261
  DEFAULTS = {
1142
1262
  gatewayUrl: "https://clixad.onrender.com",
1143
1263
  // Same origin as the gateway: the ad wall is served by the gateway process
@@ -1145,7 +1265,7 @@ var init_config = __esm({
1145
1265
  // fields stay separate so either can be pointed elsewhere, but they no longer
1146
1266
  // diverge by default.
1147
1267
  dashboardUrl: "https://clixad.onrender.com",
1148
- model: "gemini-2.5-flash-lite"
1268
+ model: FALLBACK_MODEL
1149
1269
  };
1150
1270
  ENV_OVERRIDES = [
1151
1271
  ["gatewayUrl", "CLIXAD_GATEWAY_URL"],
@@ -1188,6 +1308,12 @@ function parseLoginArgs(args) {
1188
1308
  }
1189
1309
  return { invite: invite?.trim() || void 0, email: rest[0] };
1190
1310
  }
1311
+ function adoptNewAccountModel(config, created, named) {
1312
+ if (!created || typeof named !== "string" || !/^[A-Za-z0-9._\/-]{1,64}$/.test(named)) return void 0;
1313
+ if (config.model !== FALLBACK_MODEL || named === config.model) return void 0;
1314
+ config.model = named;
1315
+ return named;
1316
+ }
1191
1317
  async function performLogin(client, config, args, report, signal) {
1192
1318
  if (!args.email) {
1193
1319
  const start = await client.deviceStart().catch(() => null);
@@ -1218,6 +1344,7 @@ async function githubLogin(client, config, start, invite, report, signal) {
1218
1344
  config.email = poll.email;
1219
1345
  config.login = poll.login;
1220
1346
  config.tokenGateway = config.gatewayUrl;
1347
+ const model = adoptNewAccountModel(config, poll.created, poll.default_model);
1221
1348
  saveConfig(config);
1222
1349
  return {
1223
1350
  status: "ok",
@@ -1226,7 +1353,8 @@ async function githubLogin(client, config, start, invite, report, signal) {
1226
1353
  login: poll.login,
1227
1354
  balance: poll.balance,
1228
1355
  created: poll.created,
1229
- signupBonus: poll.signupBonus
1356
+ signupBonus: poll.signupBonus,
1357
+ ...model ? { model } : {}
1230
1358
  }
1231
1359
  };
1232
1360
  }
@@ -1249,20 +1377,24 @@ async function devLogin(client, config, email, invite) {
1249
1377
  config.email = res.email;
1250
1378
  config.tokenGateway = config.gatewayUrl;
1251
1379
  delete config.login;
1380
+ const model = adoptNewAccountModel(config, true, res.default_model);
1252
1381
  saveConfig(config);
1253
- return { status: "ok", account: { email: res.email, balance: res.balance, created: true } };
1382
+ return {
1383
+ status: "ok",
1384
+ account: { email: res.email, balance: res.balance, created: true, ...model ? { model } : {} }
1385
+ };
1254
1386
  }
1255
1387
  function sleep2(ms, signal) {
1256
- return new Promise((resolve2) => {
1388
+ return new Promise((resolve3) => {
1257
1389
  if (!signal) {
1258
- setTimeout(resolve2, ms);
1390
+ setTimeout(resolve3, ms);
1259
1391
  return;
1260
1392
  }
1261
- if (signal.aborted) return resolve2();
1393
+ if (signal.aborted) return resolve3();
1262
1394
  const done = () => {
1263
1395
  clearTimeout(timer);
1264
1396
  signal.removeEventListener("abort", done);
1265
- resolve2();
1397
+ resolve3();
1266
1398
  };
1267
1399
  const timer = setTimeout(done, ms);
1268
1400
  signal.addEventListener("abort", done, { once: true });
@@ -1283,6 +1415,49 @@ and the Privacy Policy \u2014 ${PRIVACY_URL}`;
1283
1415
  }
1284
1416
  });
1285
1417
 
1418
+ // src/free-tier.ts
1419
+ function freeModelToName(free, current) {
1420
+ const left = (id2) => free.turns_left[id2] ?? 0;
1421
+ const usable = (id2) => id2 !== void 0 && free.models.includes(id2) && left(id2) > 0;
1422
+ const id = usable(current) ? current : usable(free.recommended) ? free.recommended : free.models.find(usable);
1423
+ return id === void 0 ? void 0 : { id, turns: left(id) };
1424
+ }
1425
+ function freeTierNotice(o) {
1426
+ const free = o.free;
1427
+ if (!free || !(free.daily_credits > 0)) return void 0;
1428
+ const pick = free.credits_left > 0 ? freeModelToName(free, o.model) : void 0;
1429
+ if (!pick) {
1430
+ return o.balance <= 0 ? "Your balance is empty and today's free turns are used up. They come back at midnight UTC, and /earn tops up before then." : void 0;
1431
+ }
1432
+ if (pick.id === o.model) {
1433
+ return `${pick.id} is free today, ${turns(pick.turns)} left. /model shows the other free models.`;
1434
+ }
1435
+ if (o.balance <= 0) {
1436
+ return `Your balance is empty, but ${pick.id} is still free today (${turns(pick.turns)}). /model switches to it, /earn tops up.`;
1437
+ }
1438
+ return `${free.models.length} models are free today. /model switches, and ${pick.id} has ${turns(pick.turns)} left.`;
1439
+ }
1440
+ function freeHelpLine(free) {
1441
+ if (!free || !(free.daily_credits > 0)) return void 0;
1442
+ if (free.credits_left <= 0) return { label: "free models", desc: "used up today, back at midnight UTC" };
1443
+ return {
1444
+ label: "free today",
1445
+ desc: `${free.models.length} models, ${free.credits_left.toLocaleString("en-US")} free credits left \xB7 /model switches`
1446
+ };
1447
+ }
1448
+ function freeAlternativeLine(freeModels, { rerun = true } = {}) {
1449
+ const first = freeModels.find((m) => m.turnsLeft > 0);
1450
+ if (!first) return void 0;
1451
+ return `${first.id} is still free today (${turns(first.turnsLeft)}). Switch with \`clixad model ${first.id}\`${rerun ? " and run it again" : ""}.`;
1452
+ }
1453
+ var turns;
1454
+ var init_free_tier = __esm({
1455
+ "src/free-tier.ts"() {
1456
+ "use strict";
1457
+ turns = (n) => `about ${n.toLocaleString("en-US")} turn${n === 1 ? "" : "s"}`;
1458
+ }
1459
+ });
1460
+
1286
1461
  // src/diff.ts
1287
1462
  function splitLines(text) {
1288
1463
  const lines = text.split("\n");
@@ -2381,7 +2556,7 @@ var init_mcp = __esm({
2381
2556
  if (this.exited) return Promise.reject(new McpError(this.exited));
2382
2557
  const id = ++this.seq;
2383
2558
  const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
2384
- return new Promise((resolve2, reject) => {
2559
+ return new Promise((resolve3, reject) => {
2385
2560
  const settle = (fn) => {
2386
2561
  clearTimeout(timer);
2387
2562
  signal?.removeEventListener("abort", onAbort);
@@ -2395,7 +2570,7 @@ var init_mcp = __esm({
2395
2570
  const onAbort = () => settle(() => reject(new McpError("cancelled")));
2396
2571
  signal?.addEventListener("abort", onAbort, { once: true });
2397
2572
  this.pending.set(id, {
2398
- resolve: (v) => settle(() => resolve2(v)),
2573
+ resolve: (v) => settle(() => resolve3(v)),
2399
2574
  reject: (e) => settle(() => reject(e))
2400
2575
  });
2401
2576
  this.child.stdin?.write(payload, (err) => {
@@ -3989,7 +4164,7 @@ var init_agent = __esm({
3989
4164
  import { spawnSync } from "node:child_process";
3990
4165
  import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
3991
4166
  import { homedir as homedir3, platform } from "node:os";
3992
- import { join as join4 } from "node:path";
4167
+ import { join as join4, parse, resolve as resolve2 } from "node:path";
3993
4168
  function contextFileCandidates(root) {
3994
4169
  return [join4(homedir3(), ".clixad", "CLIXAD.md"), join4(root, "AGENTS.md"), join4(root, "CLIXAD.md")];
3995
4170
  }
@@ -4050,13 +4225,21 @@ function topLevel(root) {
4050
4225
  return void 0;
4051
4226
  }
4052
4227
  }
4053
- var MAX_CONTEXT_FILE_CHARS, INIT_PROMPT;
4228
+ function shouldSuggestInit(root) {
4229
+ const dir = resolve2(root);
4230
+ if (dir === resolve2(homedir3()) || dir === parse(dir).root) return false;
4231
+ if (existsSync3(join4(dir, "CLIXAD.md")) || existsSync3(join4(dir, "AGENTS.md"))) return false;
4232
+ return PROJECT_MARKERS.some((name) => existsSync3(join4(dir, name)));
4233
+ }
4234
+ var MAX_CONTEXT_FILE_CHARS, INIT_PROMPT, INIT_HINT, PROJECT_MARKERS;
4054
4235
  var init_context = __esm({
4055
4236
  "src/context.ts"() {
4056
4237
  "use strict";
4057
4238
  init_agent();
4058
4239
  MAX_CONTEXT_FILE_CHARS = 2e4;
4059
4240
  INIT_PROMPT = "Create a CLIXAD.md file in the workspace root that tells a future coding agent how to work in this project. Explore the repository first (glob, grep, read the README and package manifests). Keep it under 60 lines and cover: what the project is, the layout, how to build/test/run it, and the conventions a newcomer would otherwise get wrong. If CLIXAD.md already exists, improve it instead of replacing it wholesale.";
4241
+ INIT_HINT = "No CLIXAD.md in this project yet. /init writes one, and every later session here starts out knowing the layout.";
4242
+ PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile"];
4060
4243
  }
4061
4244
  });
4062
4245
 
@@ -4150,7 +4333,18 @@ var init_permissions = __esm({
4150
4333
  });
4151
4334
 
4152
4335
  // src/session.ts
4153
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
4336
+ import {
4337
+ closeSync as closeSync2,
4338
+ existsSync as existsSync4,
4339
+ mkdirSync as mkdirSync3,
4340
+ openSync as openSync2,
4341
+ readdirSync as readdirSync3,
4342
+ readFileSync as readFileSync5,
4343
+ readSync,
4344
+ rmSync,
4345
+ statSync as statSync2,
4346
+ writeFileSync as writeFileSync3
4347
+ } from "node:fs";
4154
4348
  import { homedir as homedir4 } from "node:os";
4155
4349
  import { join as join5 } from "node:path";
4156
4350
  function newSessionId() {
@@ -4187,6 +4381,28 @@ function listSessions(cwd, limit = 20) {
4187
4381
  metas.sort((a, b) => b.updated.localeCompare(a.updated));
4188
4382
  return metas.slice(0, limit);
4189
4383
  }
4384
+ function hasSessionIn(cwd) {
4385
+ if (!existsSync4(SESSION_DIR)) return false;
4386
+ const needle = `"cwd":${JSON.stringify(cwd)}`;
4387
+ try {
4388
+ for (const name of readdirSync3(SESSION_DIR)) {
4389
+ if (!name.endsWith(".json")) continue;
4390
+ const path = join5(SESSION_DIR, name);
4391
+ let head = "";
4392
+ const fd = openSync2(path, "r");
4393
+ try {
4394
+ const buf = Buffer.alloc(4096);
4395
+ head = buf.subarray(0, readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
4396
+ } finally {
4397
+ closeSync2(fd);
4398
+ }
4399
+ if (head.includes(needle)) return true;
4400
+ if (!head.includes('"cwd":') && loadSession(name.slice(0, -5))?.cwd === cwd) return true;
4401
+ }
4402
+ } catch {
4403
+ }
4404
+ return false;
4405
+ }
4190
4406
  function latestSession(cwd) {
4191
4407
  const [meta] = listSessions(cwd, 1);
4192
4408
  return meta ? loadSession(meta.id) : void 0;
@@ -4284,14 +4500,14 @@ function runKimiCode(config, task, opts = {}) {
4284
4500
  OPENAI_BASE_URL: `${config.gatewayUrl.replace(/\/+$/, "")}/v1`,
4285
4501
  OPENAI_API_KEY: config.token
4286
4502
  };
4287
- return new Promise((resolve2) => {
4503
+ return new Promise((resolve3) => {
4288
4504
  const child = process.platform === "win32" ? spawn5([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn5(bin, args, { stdio: "inherit", env });
4289
4505
  child.on("error", (err) => {
4290
4506
  if (err.code === "ENOENT") console.log("\n" + INSTALL_HELP);
4291
4507
  else console.error(`failed to launch Kimi CLI: ${err.message}`);
4292
- resolve2(127);
4508
+ resolve3(127);
4293
4509
  });
4294
- child.on("exit", (code) => resolve2(code ?? 0));
4510
+ child.on("exit", (code) => resolve3(code ?? 0));
4295
4511
  });
4296
4512
  }
4297
4513
  function quoteWin(s) {
@@ -4804,6 +5020,16 @@ function sponsorEnabled({ isTTY, env, configEnabled }) {
4804
5020
  function adFitsWidth(cols) {
4805
5021
  return cols >= AD_MIN_COLS;
4806
5022
  }
5023
+ function adFitsFormat(format, cols) {
5024
+ return format === "sidebar_card" ? cols >= SIDEBAR_MIN_COLS : adFitsWidth(cols);
5025
+ }
5026
+ function sidebarCard(line2, cols) {
5027
+ if (!adFitsFormat("sidebar_card", cols)) return null;
5028
+ const text = line2.trim();
5029
+ if (!text) return null;
5030
+ const body = visibleLength(text) <= SIDEBAR_BODY_COLS ? text : `${sliceColumns(text, SIDEBAR_BODY_COLS - 1).trimEnd()}\u2026`;
5031
+ return { rule: SIDEBAR_RULE, label: `${SPONSOR_SIGIL} ${AD_LABEL}`, body };
5032
+ }
4807
5033
  function sponsorRow(line2, cols, origin) {
4808
5034
  const marker = origin === "house" || cols >= AD_FULL_PREFIX_MIN_COLS ? HOUSE_MARKER_ROW : AD_MARKER_ROW;
4809
5035
  const label = origin === "network" ? LABEL_ROW : null;
@@ -4818,7 +5044,7 @@ function adColorEnabled({ isTTY, env }) {
4818
5044
  if (env.CI !== void 0 && env.CI !== "") return false;
4819
5045
  return true;
4820
5046
  }
4821
- var SPONSOR_SIGIL, SPONSOR_MARKER, AD_LABEL, INDENT, HOUSE_MARKER_ROW, AD_MARKER_ROW, LABEL_ROW, MIN_BODY_COLS, SHORT_AD_COLS, AD_MIN_COLS, AD_FULL_PREFIX_MIN_COLS;
5047
+ var SPONSOR_SIGIL, SPONSOR_MARKER, AD_LABEL, INDENT, HOUSE_MARKER_ROW, AD_MARKER_ROW, LABEL_ROW, MIN_BODY_COLS, SHORT_AD_COLS, AD_MIN_COLS, AD_FULL_PREFIX_MIN_COLS, SIDEBAR_BODY_COLS, SIDEBAR_RULE, SIDEBAR_CARD_COLS, SIDEBAR_MIN_COLS;
4822
5048
  var init_render = __esm({
4823
5049
  "src/sponsor/render.ts"() {
4824
5050
  "use strict";
@@ -4835,6 +5061,10 @@ var init_render = __esm({
4835
5061
  SHORT_AD_COLS = 29;
4836
5062
  AD_MIN_COLS = visibleLength(AD_MARKER_ROW) + visibleLength(LABEL_ROW) + SHORT_AD_COLS;
4837
5063
  AD_FULL_PREFIX_MIN_COLS = visibleLength(HOUSE_MARKER_ROW) + visibleLength(LABEL_ROW) + SHORT_AD_COLS;
5064
+ SIDEBAR_BODY_COLS = SHORT_AD_COLS + 9;
5065
+ SIDEBAR_RULE = "\u2502 ";
5066
+ SIDEBAR_CARD_COLS = visibleLength(SIDEBAR_RULE) + SIDEBAR_BODY_COLS;
5067
+ SIDEBAR_MIN_COLS = 50 + SIDEBAR_CARD_COLS;
4838
5068
  }
4839
5069
  });
4840
5070
 
@@ -4845,7 +5075,7 @@ function sponsorLineText(ad) {
4845
5075
  if (!text) return void 0;
4846
5076
  return text.length > MAX_TEXT_CHARS3 ? text.slice(0, MAX_TEXT_CHARS3).trimEnd() : text;
4847
5077
  }
4848
- function createSponsorSlot(config) {
5078
+ function createSponsorSlot(config, format = "top_bar") {
4849
5079
  const slot = new SponsorSlot({
4850
5080
  publisherId: config.publisherId,
4851
5081
  serveOnly: true,
@@ -4868,7 +5098,7 @@ function createSponsorSlot(config) {
4868
5098
  });
4869
5099
  return {
4870
5100
  async serve(cols, signal) {
4871
- if (!adFitsWidth(cols)) return null;
5101
+ if (!adFitsFormat(format, cols)) return null;
4872
5102
  const ad = await slot.next({
4873
5103
  ...config.subject ? { subject: config.subject } : {},
4874
5104
  ...isCategory(config.category) ? { category: config.category } : {}
@@ -4900,9 +5130,11 @@ var init_network = __esm({
4900
5130
  function commandLabel(c2) {
4901
5131
  return `/${c2.name}${c2.args ? ` ${c2.args}` : ""}`;
4902
5132
  }
4903
- function helpText() {
4904
- const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length));
4905
- return COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
5133
+ function helpText(free) {
5134
+ const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length), free ? free.label.length : 0);
5135
+ return (free ? ` ${free.label.padEnd(w + 2)}${free.desc}
5136
+
5137
+ ` : "") + COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
4906
5138
  " @path attach a file to the message (tab completes)",
4907
5139
  " !command run a shell command yourself, no model call",
4908
5140
  " shift+tab cycle permission mode \xB7 esc stop the current turn",
@@ -5387,7 +5619,7 @@ var init_views = __esm({
5387
5619
  // src/tui/app.tsx
5388
5620
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5389
5621
  import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
5390
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5622
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5391
5623
  function buyDeclarationLines(pack) {
5392
5624
  return [
5393
5625
  `${pack.label} \u2014 ${pack.credits.toLocaleString("en-US")} credits for $${pack.price_usd.toFixed(2)}`,
@@ -5430,7 +5662,18 @@ function useTerminalSize() {
5430
5662
  }, [stdout]);
5431
5663
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5432
5664
  }
5433
- function App({ client, config, wallet, streak, session, initialTask, notices: notices2, sponsorServe }) {
5665
+ function App({
5666
+ client,
5667
+ config,
5668
+ wallet,
5669
+ streak,
5670
+ session,
5671
+ initialTask,
5672
+ notices: notices2,
5673
+ sponsorServe,
5674
+ sponsorFormats,
5675
+ initHint
5676
+ }) {
5434
5677
  const { exit } = useApp();
5435
5678
  const { stdout, write: writeToStdout } = useStdout();
5436
5679
  const idRef = useRef(1);
@@ -5466,7 +5709,12 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5466
5709
  const [spent, setSpent] = useState(0);
5467
5710
  const [freeTier, setFreeTier] = useState(wallet?.free_tier);
5468
5711
  const [model, setModel2] = useState(config.model);
5712
+ const modelRef = useRef(config.model);
5469
5713
  const [mode, setMode] = useState("normal");
5714
+ const switchModel = useCallback((id) => {
5715
+ modelRef.current = id;
5716
+ setModel2(id);
5717
+ }, []);
5470
5718
  const [hist, setHist] = useState(() => loadHistory());
5471
5719
  const [histIdx, setHistIdx] = useState(-1);
5472
5720
  const [sel, setSel] = useState(0);
@@ -5480,7 +5728,12 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5480
5728
  const [startedAt, setStartedAt] = useState(0);
5481
5729
  const [quitHint, setQuitHint] = useState(false);
5482
5730
  const [busyLabel, setBusyLabel] = useState(WORKING);
5483
- const [sponsor, setSponsor] = useState(null);
5731
+ const [sponsor, setSponsor] = useState(
5732
+ null
5733
+ );
5734
+ const [adSlots, setAdSlots] = useState(
5735
+ {}
5736
+ );
5484
5737
  const messagesRef = useRef(messages);
5485
5738
  messagesRef.current = messages;
5486
5739
  const busyRef = useRef(false);
@@ -5506,10 +5759,15 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5506
5759
  const spinnerRef = useRef(spinnerFrames());
5507
5760
  const runningToolRef = useRef(null);
5508
5761
  const pendingTaskRef = useRef(null);
5762
+ const pendingFreeRef = useRef([]);
5763
+ const pendingGrantRef = useRef(0);
5509
5764
  const mcpRef = useRef(McpHub.empty());
5510
5765
  const todosRef = useRef(new TodoList());
5511
5766
  const lastTurnRef = useRef(null);
5512
5767
  const freeSpentNoticeRef = useRef(false);
5768
+ const emptyNoticeRef = useRef(false);
5769
+ const initHintedRef = useRef(false);
5770
+ const sessionTurnsRef = useRef(0);
5513
5771
  const turnCountRef = useRef(config.turns ?? 0);
5514
5772
  const deltaBufRef = useRef("");
5515
5773
  const deltaTimerRef = useRef(null);
@@ -5517,36 +5775,54 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5517
5775
  const sponsorIdxRef = useRef(0);
5518
5776
  const sponsorPrevRef = useRef(void 0);
5519
5777
  const sponsorCallRef = useRef(0);
5520
- const pendingImpressionRef = useRef(null);
5521
- const sponsorSlotRef = useRef(null);
5778
+ const pendingImpressionRef = useRef(/* @__PURE__ */ new Map());
5779
+ const drawnRef = useRef(/* @__PURE__ */ new Set());
5780
+ const sponsorSlotRef = useRef(
5781
+ /* @__PURE__ */ new Map()
5782
+ );
5522
5783
  const colsRef = useRef(cols);
5523
5784
  colsRef.current = cols;
5785
+ const modalOpenRef = useRef(false);
5524
5786
  const sponsorConfigRef = useRef(null);
5525
- const remoteSponsorRef = useRef(
5526
- adtentionSource(async (signal) => {
5527
- let served;
5528
- if (sponsorServe) {
5529
- served = await sponsorServe(colsRef.current, signal);
5530
- } else {
5531
- sponsorConfigRef.current ??= client.sponsorConfig(signal);
5532
- const conf = await sponsorConfigRef.current;
5533
- if (!conf) return null;
5534
- sponsorSlotRef.current ??= createSponsorSlot(conf);
5535
- served = await sponsorSlotRef.current.serve(colsRef.current, signal);
5787
+ const serveFormat = async (format, signal) => {
5788
+ let served;
5789
+ if (sponsorServe) {
5790
+ if (format !== "top_bar" && !sponsorFormats?.includes(format)) return null;
5791
+ served = await sponsorServe(colsRef.current, signal, format);
5792
+ } else {
5793
+ sponsorConfigRef.current ??= client.sponsorConfig(signal);
5794
+ const conf = await sponsorConfigRef.current;
5795
+ if (!conf) return null;
5796
+ const publisherId = format === "top_bar" ? conf.publisherId : conf.formats?.[format];
5797
+ if (!publisherId) return null;
5798
+ let slot = sponsorSlotRef.current.get(format);
5799
+ if (!slot) {
5800
+ slot = createSponsorSlot({ ...conf, publisherId }, format);
5801
+ sponsorSlotRef.current.set(format, slot);
5536
5802
  }
5537
- if (!served) return null;
5538
- return {
5539
- id: served.id,
5540
- text: served.text,
5541
- impression: {
5542
- impressionId: served.impressionId,
5543
- adId: served.id,
5544
- billable: served.billable,
5545
- fromCache: served.fromCache,
5546
- credit: served.credit
5547
- }
5548
- };
5549
- })
5803
+ served = await slot.serve(colsRef.current, signal);
5804
+ }
5805
+ if (!served) return null;
5806
+ return {
5807
+ id: served.id,
5808
+ text: served.text,
5809
+ impression: {
5810
+ impressionId: served.impressionId,
5811
+ ...format === "top_bar" ? {} : { format },
5812
+ adId: served.id,
5813
+ billable: served.billable,
5814
+ fromCache: served.fromCache,
5815
+ credit: served.credit
5816
+ }
5817
+ };
5818
+ };
5819
+ const remoteSponsorRef = useRef(
5820
+ Object.fromEntries(
5821
+ ["top_bar", ...EXTRA_AD_FORMATS].map((format) => [
5822
+ format,
5823
+ adtentionSource((signal) => serveFormat(format, signal))
5824
+ ])
5825
+ )
5550
5826
  );
5551
5827
  const tallyRef = useRef(createTally());
5552
5828
  const heightCacheRef = useRef(/* @__PURE__ */ new Map());
@@ -5593,9 +5869,17 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5593
5869
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
5594
5870
  const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
5595
5871
  if (streakLine) push({ kind: "notice", tone: "good", text: ` ${streakLine}` });
5872
+ const freeLine = freeTierNotice({ model: config.model, balance: wallet?.balance ?? 0, free: wallet?.free_tier });
5873
+ if (freeLine) {
5874
+ push({ kind: "notice", tone: (wallet?.balance ?? 0) <= 0 ? "warn" : "good", text: ` ${freeLine}` });
5875
+ }
5596
5876
  if (session?.messages.length) {
5597
5877
  push({ kind: "notice", text: ` resumed session ${session.id} (${session.messages.length} messages)` });
5598
5878
  }
5879
+ if (initHint?.returning) {
5880
+ initHintedRef.current = true;
5881
+ push({ kind: "notice", text: ` ${INIT_HINT}` });
5882
+ }
5599
5883
  client.models().then((m) => {
5600
5884
  catalogRef.current = m;
5601
5885
  searchRef.current = client.capabilities?.web_search === true;
@@ -5645,6 +5929,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5645
5929
  configEnabled: config.sponsor
5646
5930
  })) {
5647
5931
  setSponsor(null);
5932
+ setAdSlots({});
5648
5933
  return;
5649
5934
  }
5650
5935
  const est = catalogRef.current.find((m) => m.id === modelId)?.est_ads_per_task;
@@ -5660,31 +5945,47 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5660
5945
  } else {
5661
5946
  setSponsor(null);
5662
5947
  }
5663
- if (!adFitsWidth(colsRef.current)) return;
5664
- void remoteSponsorRef.current.fetch().then((line2) => {
5665
- if (!line2 || call !== sponsorCallRef.current) return;
5666
- if (!sponsorRow(line2.text, colsRef.current, "network")) return;
5667
- sponsorPrevRef.current = line2.id;
5668
- recordImpression(tallyRef.current, line2.id);
5669
- setSponsor({ text: line2.text, origin: "network" });
5670
- pendingImpressionRef.current = { call, impression: line2.impression };
5671
- });
5948
+ for (const format of ["top_bar", ...EXTRA_AD_FORMATS]) {
5949
+ if (!adFitsFormat(format, colsRef.current)) continue;
5950
+ void remoteSponsorRef.current[format].fetch().then((line2) => {
5951
+ if (!line2 || call !== sponsorCallRef.current) return;
5952
+ if (!adFitsFormat(format, colsRef.current)) return;
5953
+ if (format !== "top_bar" && modalOpenRef.current) return;
5954
+ if (format === "top_bar") {
5955
+ if (!sponsorRow(line2.text, colsRef.current, "network")) return;
5956
+ sponsorPrevRef.current = line2.id;
5957
+ setSponsor({ text: line2.text, origin: "network", impressionId: line2.impression.impressionId });
5958
+ } else {
5959
+ setAdSlots((slots) => ({
5960
+ ...slots,
5961
+ [format]: { text: line2.text, impressionId: line2.impression.impressionId }
5962
+ }));
5963
+ }
5964
+ recordImpression(tallyRef.current, line2.id);
5965
+ pendingImpressionRef.current.set(format, { call, impression: line2.impression });
5966
+ });
5967
+ }
5672
5968
  },
5673
5969
  [config.sponsor, stdout, wallet]
5674
5970
  );
5675
5971
  const reportSponsor = useCallback(() => {
5676
- const pending = pendingImpressionRef.current;
5677
- pendingImpressionRef.current = null;
5678
- if (!pending || pending.call !== sponsorCallRef.current) return;
5679
- void client.reportImpression(pending.impression);
5972
+ const pending = [...pendingImpressionRef.current.values()];
5973
+ const drawn = drawnRef.current;
5974
+ pendingImpressionRef.current = /* @__PURE__ */ new Map();
5975
+ drawnRef.current = /* @__PURE__ */ new Set();
5976
+ const reports = pending.filter((p) => p.call === sponsorCallRef.current).filter((p) => drawn.has(p.impression.impressionId)).map((p) => p.impression);
5977
+ if (reports.length === 0) return;
5978
+ void (async () => {
5979
+ for (const report of reports) await client.reportImpression(report);
5980
+ })();
5680
5981
  }, [client]);
5681
5982
  const askUser = useCallback(
5682
- (req) => new Promise((resolve2) => setAsk({ req, resolve: resolve2 })),
5983
+ (req) => new Promise((resolve3) => setAsk({ req, resolve: resolve3 })),
5683
5984
  []
5684
5985
  );
5685
5986
  const permit = useMemo(() => createPermit(() => permRef.current, askUser), [askUser]);
5686
5987
  const approvePlan = useCallback(
5687
- (proposed) => new Promise((resolve2) => {
5988
+ (proposed) => new Promise((resolve3) => {
5688
5989
  setPlan({
5689
5990
  plan: proposed,
5690
5991
  resolve: (decision) => {
@@ -5692,7 +5993,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5692
5993
  permRef.current.mode = decision;
5693
5994
  setMode(decision);
5694
5995
  }
5695
- resolve2(decision);
5996
+ resolve3(decision);
5696
5997
  }
5697
5998
  });
5698
5999
  }),
@@ -5704,7 +6005,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5704
6005
  setMode(next);
5705
6006
  }, []);
5706
6007
  const askQuestion = useCallback(
5707
- (req) => new Promise((resolve2) => {
6008
+ (req) => new Promise((resolve3) => {
5708
6009
  setPickerSel(0);
5709
6010
  setPicker({
5710
6011
  title: req.question,
@@ -5716,10 +6017,10 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5716
6017
  })),
5717
6018
  onPick: (choice) => {
5718
6019
  push({ kind: "notice", tone: "good", text: ` answered: ${choice.value}` });
5719
- resolve2(choice.value);
6020
+ resolve3(choice.value);
5720
6021
  },
5721
6022
  onCancel: () => {
5722
- resolve2("nothing \u2014 the user dismissed the question. Do not ask it again.");
6023
+ resolve3("nothing \u2014 the user dismissed the question. Do not ask it again.");
5723
6024
  stopCurrent();
5724
6025
  }
5725
6026
  });
@@ -5798,17 +6099,19 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5798
6099
  );
5799
6100
  const runTurn2 = useCallback(
5800
6101
  async (task, images = []) => {
6102
+ pendingFreeRef.current = [];
6103
+ const model2 = modelRef.current;
5801
6104
  const ac = new AbortController();
5802
6105
  abortRef.current = ac;
5803
6106
  setBusy(true);
5804
6107
  setStartedAt(Date.now());
5805
6108
  setLive({ text: "" });
5806
- nextSponsor(model);
6109
+ nextSponsor(model2);
5807
6110
  let history = messagesRef.current;
5808
6111
  try {
5809
- const window = contextWindow(model);
6112
+ const window = contextWindow(model2);
5810
6113
  if (shouldCompact([{ role: "system", content: contextRef.current.systemPrompt }, ...history], window)) {
5811
- const res = await compact(client, model, [
6114
+ const res = await compact(client, model2, [
5812
6115
  { role: "system", content: contextRef.current.systemPrompt },
5813
6116
  ...history
5814
6117
  ], { signal: ac.signal });
@@ -5824,7 +6127,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5824
6127
  }
5825
6128
  }
5826
6129
  const result = await runAgent(client, task, {
5827
- model,
6130
+ model: model2,
5828
6131
  root,
5829
6132
  history,
5830
6133
  attachments: images,
@@ -5866,7 +6169,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5866
6169
  started: sessionRef.current.started,
5867
6170
  updated: (/* @__PURE__ */ new Date()).toISOString(),
5868
6171
  cwd: root,
5869
- model,
6172
+ model: model2,
5870
6173
  title: sessionRef.current.title,
5871
6174
  messages: next
5872
6175
  });
@@ -5882,8 +6185,10 @@ ${NO_SEARCH_INSTRUCTION}`),
5882
6185
  text: result.content,
5883
6186
  meta: turnMeta(result)
5884
6187
  });
5885
- if (freeTier?.models.includes(model) && result.creditsWaived === 0 && result.creditsCharged > 0 && (result.freeCreditsLeft ?? freeTier.credits_left) === 0 && !freeSpentNoticeRef.current) {
6188
+ let spentNotice = false;
6189
+ if (freeTier?.models.includes(model2) && result.creditsWaived === 0 && result.creditsCharged > 0 && (result.freeCreditsLeft ?? freeTier.credits_left) === 0 && !freeSpentNoticeRef.current) {
5886
6190
  freeSpentNoticeRef.current = true;
6191
+ spentNotice = true;
5887
6192
  push({
5888
6193
  kind: "notice",
5889
6194
  tone: "warn",
@@ -5891,9 +6196,18 @@ ${NO_SEARCH_INSTRUCTION}`),
5891
6196
  The free allowance resets at midnight UTC. /earn tops up before then.`
5892
6197
  });
5893
6198
  }
6199
+ if (result.creditsCharged > 0 && result.balance <= 0 && !spentNotice && !emptyNoticeRef.current) {
6200
+ emptyNoticeRef.current = true;
6201
+ const on = model2;
6202
+ void client.wallet().then((w) => {
6203
+ setFreeTier(w.free_tier);
6204
+ const line2 = freeTierNotice({ model: on, balance: w.balance, free: w.free_tier });
6205
+ if (line2) push({ kind: "notice", tone: "warn", text: ` ${line2}` });
6206
+ }).catch(() => void 0);
6207
+ }
5894
6208
  lastTurnRef.current = {
5895
6209
  turn: turnCountRef.current,
5896
- model,
6210
+ model: model2,
5897
6211
  prompt: task,
5898
6212
  response: result.content,
5899
6213
  steps: result.steps,
@@ -5906,14 +6220,26 @@ ${NO_SEARCH_INSTRUCTION}`),
5906
6220
  turnCountRef.current += 1;
5907
6221
  config.turns = turnCountRef.current;
5908
6222
  saveConfig(config);
6223
+ sessionTurnsRef.current += 1;
6224
+ if (initHint && !initHintedRef.current && sessionTurnsRef.current >= INIT_HINT_AFTER_TURNS && shouldSuggestInit(root)) {
6225
+ initHintedRef.current = true;
6226
+ push({ kind: "notice", text: ` ${INIT_HINT}` });
6227
+ }
5909
6228
  }
5910
6229
  } catch (err) {
5911
6230
  if (err instanceof PaywallError) {
5912
6231
  pendingTaskRef.current = { task, images };
6232
+ pendingFreeRef.current = err.freeModels;
6233
+ pendingGrantRef.current = err.grantPerAd;
6234
+ const free = err.freeModels[0];
5913
6235
  push({
5914
6236
  kind: "notice",
5915
6237
  tone: "warn",
5916
- text: ` Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).
6238
+ text: free ? ` Out of credits on ${model2} (balance ${err.balance}, need ~${err.estimatedCost}).
6239
+ Press enter \u2014 ${free.id} is still free today (~${free.turnsLeft} turn${free.turnsLeft === 1 ? "" : "s"} left), or take an offer instead.
6240
+ Your request is kept either way, so you will not have to type it again.
6241
+ Offers you are screened out of pay nothing; that is normal, just start another.
6242
+ /buy is there too, if you would rather pay than do an offer.` : ` Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).
5917
6243
  Press enter to open the ad wall \u2014 offers pay ${grantLabel(err.grantPerAd)}.
5918
6244
  I'll continue automatically. Offers you are screened out of pay nothing;
5919
6245
  that is normal, just start another.
@@ -5932,6 +6258,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5932
6258
  reportSponsor();
5933
6259
  sponsorCallRef.current++;
5934
6260
  setSponsor(null);
6261
+ setAdSlots({});
5935
6262
  }
5936
6263
  },
5937
6264
  [
@@ -6002,10 +6329,21 @@ ${NO_SEARCH_INSTRUCTION}`),
6002
6329
  tone: "good",
6003
6330
  text: ` signed in as ${account.login ?? account.email ?? "you"} \xB7 balance ${account.balance.toLocaleString("en-US")} credits` + (account.created ? " (signup bonus)" : "")
6004
6331
  });
6332
+ if (account.model) {
6333
+ switchModel(account.model);
6334
+ push({ kind: "notice", text: ` model \u2192 ${account.model} (the default for new accounts)` });
6335
+ }
6005
6336
  const claimed = await client.claimStreak(signal);
6006
6337
  const line2 = claimed?.claimed ? streakLabel(claimed) : null;
6007
6338
  if (claimed?.claimed) setBalance(claimed.balance);
6008
6339
  if (line2) push({ kind: "notice", tone: "good", text: ` ${line2}` });
6340
+ const w = await client.wallet(signal).catch(() => void 0);
6341
+ if (w) {
6342
+ setBalance(w.balance);
6343
+ setFreeTier(w.free_tier);
6344
+ const freeLine = freeTierNotice({ model: modelRef.current, balance: w.balance, free: w.free_tier });
6345
+ if (freeLine) push({ kind: "notice", tone: w.balance <= 0 ? "warn" : "good", text: ` ${freeLine}` });
6346
+ }
6009
6347
  return;
6010
6348
  }
6011
6349
  case "closed":
@@ -6073,6 +6411,46 @@ ${NO_SEARCH_INSTRUCTION}`),
6073
6411
  push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
6074
6412
  if (pending) await runTurn2(pending.task, pending.images);
6075
6413
  }, [balance, client, config.dashboardUrl, push, runBusy, runTurn2]);
6414
+ const runPaywallChoice = useCallback(() => {
6415
+ const free = pendingFreeRef.current.filter((m) => m.id !== EARN_ROW);
6416
+ if (free.length === 0) return void runAdWall();
6417
+ const items = [
6418
+ ...free.map((m) => ({
6419
+ value: m.id,
6420
+ label: m.id,
6421
+ hint: `free: ~${m.turnsLeft} turn${m.turnsLeft === 1 ? "" : "s"} left today`,
6422
+ current: m.id === model
6423
+ })),
6424
+ {
6425
+ value: EARN_ROW,
6426
+ label: "earn credits instead",
6427
+ // The wall keeps its figure here: this row is the one that costs time,
6428
+ // and what it pays is the only thing that makes the time worth it.
6429
+ hint: `opens the ad wall \u2014 offers pay ${grantLabel(pendingGrantRef.current)}`
6430
+ }
6431
+ ];
6432
+ setPickerSel(0);
6433
+ setPicker({
6434
+ title: "Out of credits",
6435
+ subtitle: "Pick a free model to carry on with, or earn credits at the wall.",
6436
+ items,
6437
+ onPick: (choice) => {
6438
+ if (choice.value === EARN_ROW) return void runAdWall();
6439
+ const pending = pendingTaskRef.current;
6440
+ pendingTaskRef.current = null;
6441
+ pendingFreeRef.current = [];
6442
+ config.model = choice.value;
6443
+ saveConfig(config);
6444
+ switchModel(choice.value);
6445
+ push({
6446
+ kind: "notice",
6447
+ tone: "good",
6448
+ text: ` model \u2192 ${choice.value} (free today) \u2014 continuing where you left off`
6449
+ });
6450
+ if (pending) void runTurn2(pending.task, pending.images);
6451
+ }
6452
+ });
6453
+ }, [config, model, push, runAdWall, runTurn2, switchModel]);
6076
6454
  const runBuy = useCallback(async () => {
6077
6455
  await runBusy(LOADING, async (signal) => {
6078
6456
  const { enabled, packs } = await client.creditPacks(signal);
@@ -6160,7 +6538,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6160
6538
  exit();
6161
6539
  return;
6162
6540
  case "help":
6163
- push({ kind: "notice", text: helpText() });
6541
+ push({ kind: "notice", text: helpText(freeHelpLine(freeTier)) });
6164
6542
  return;
6165
6543
  // Clears both halves of what "clear" means: the model's context *and*
6166
6544
  // the screen. It used to drop only the context, so nothing visibly
@@ -6216,6 +6594,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6216
6594
  });
6217
6595
  return;
6218
6596
  case "init":
6597
+ initHintedRef.current = true;
6219
6598
  await runTurn2(INIT_PROMPT);
6220
6599
  return;
6221
6600
  // Always the picker — an id you have to remember and type is exactly
@@ -6248,7 +6627,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6248
6627
  onPick: (choice) => {
6249
6628
  config.model = choice.value;
6250
6629
  saveConfig(config);
6251
- setModel2(choice.value);
6630
+ switchModel(choice.value);
6252
6631
  push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
6253
6632
  }
6254
6633
  });
@@ -6391,8 +6770,8 @@ ${NO_SEARCH_INSTRUCTION}`),
6391
6770
  // stop" whenever it is locked.
6392
6771
  case "refer":
6393
6772
  await runBusy(LOADING, async (signal) => {
6394
- const info = await client.referral(signal);
6395
- push({ kind: "notice", text: referLines(info).map((line3) => ` ${line3}`).join("\n") });
6773
+ const info2 = await client.referral(signal);
6774
+ push({ kind: "notice", text: referLines(info2).map((line3) => ` ${line3}`).join("\n") });
6396
6775
  });
6397
6776
  return;
6398
6777
  case "buy":
@@ -6488,7 +6867,7 @@ ${output}` }
6488
6867
  lastTurnRef.current = null;
6489
6868
  const amount = turn.free ? wallet?.rewards?.feedback_credits_free ?? wallet?.rewards?.feedback_credits : wallet?.rewards?.feedback_credits;
6490
6869
  const pay = amount ? `Both answers pay ${amount.toLocaleString("en-US")} credits` : "Both answers pay the same";
6491
- const choice = await new Promise((resolve2) => {
6870
+ const choice = await new Promise((resolve3) => {
6492
6871
  setPickerSel(0);
6493
6872
  setPicker({
6494
6873
  title: "Was that last answer any good?",
@@ -6498,10 +6877,10 @@ ${output}` }
6498
6877
  { value: "down", label: "no \u2014 that missed", hint: "" },
6499
6878
  { value: "skip", label: "skip", hint: "nothing is sent, nothing is stored" }
6500
6879
  ],
6501
- onPick: (item) => resolve2(item.value),
6880
+ onPick: (item) => resolve3(item.value),
6502
6881
  // Esc and "skip" are the same outcome and must stay that way: a skip
6503
6882
  // that cost something is a prompt people stop reading.
6504
- onCancel: () => resolve2(null)
6883
+ onCancel: () => resolve3(null)
6505
6884
  });
6506
6885
  });
6507
6886
  if (choice !== "up" && choice !== "down") return;
@@ -6752,7 +7131,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6752
7131
  if (key.return) {
6753
7132
  if (key.meta || key.shift) return setEditor(newline(editor));
6754
7133
  if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
6755
- if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
7134
+ if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runPaywallChoice();
6756
7135
  return void submit(toText(editor));
6757
7136
  }
6758
7137
  if (key.ctrl && ch === "j") return setEditor(newline(editor));
@@ -6777,6 +7156,10 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6777
7156
  const spinner = spinnerRef.current[Math.floor(tick / SPINNER_TICKS_PER_FRAME) % spinnerRef.current.length];
6778
7157
  const sponsorLine = busy && sponsor ? sponsorRow(sponsor.text, cols, sponsor.origin) : null;
6779
7158
  const adColor = adColorEnabled({ isTTY: stdout.isTTY, env: process.env });
7159
+ const modalOpen = Boolean(picker || ask2 || plan || buy);
7160
+ modalOpenRef.current = modalOpen;
7161
+ const card = busy && !modalOpen && adSlots.sidebar_card ? sidebarCard(adSlots.sidebar_card.text, cols) : null;
7162
+ const statusBarLine = busy && !modalOpen && adSlots.status_bar ? sponsorRow(adSlots.status_bar.text, cols, "network") : null;
6780
7163
  const viewport = Math.max(1, rows - 1);
6781
7164
  const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
6782
7165
  const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
@@ -6798,8 +7181,18 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6798
7181
  const showStatus = grant(2) > 0;
6799
7182
  const queueRows = grant(queuedRows.length) > 0 ? queuedRows : [];
6800
7183
  const showSponsor = grant(sponsorLine ? 1 : 0) > 0;
7184
+ const showStatusBar = grant(statusBarLine && showStatus && !quitHint && menu.length === 0 ? 1 : 0) > 0;
7185
+ const showCard = busyHeight > 0 && card !== null;
6801
7186
  const inputBoxHeight = inputRows + (inputBorder ? 2 : 0);
6802
- const chromeHeight = inputBoxHeight + (showStatus ? 2 : 0) + (showSponsor ? 1 : 0) + queueRows.length;
7187
+ const chromeHeight = inputBoxHeight + (showStatus ? 2 : 0) + (showSponsor ? 1 : 0) + (showStatusBar ? 1 : 0) + queueRows.length;
7188
+ const drawnKey = [
7189
+ showSponsor && sponsorLine?.origin === "network" ? sponsor?.impressionId : void 0,
7190
+ showCard ? adSlots.sidebar_card?.impressionId : void 0,
7191
+ showStatusBar ? adSlots.status_bar?.impressionId : void 0
7192
+ ].filter(Boolean).join(" ");
7193
+ useEffect(() => {
7194
+ for (const id of drawnKey.split(" ")) if (id) drawnRef.current.add(id);
7195
+ }, [drawnKey]);
6803
7196
  let budget = Math.max(0, viewport - chromeHeight - menu.length - busyHeight);
6804
7197
  const pickerCompact = Boolean(picker) && budget < PICKER_CHROME + 1;
6805
7198
  const pickerChrome = pickerCompact ? PICKER_CHROME_COMPACT : PICKER_CHROME;
@@ -6887,15 +7280,31 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6887
7280
  Math.max(1, Math.floor(cols * 0.5)),
6888
7281
  menu.reduce((w, c2) => Math.max(w, visibleLength(c2.label)), 0)
6889
7282
  );
7283
+ const conversationUsed = useMemo(
7284
+ () => conversationTokens([{ role: "system", content: contextRef.current.systemPrompt }, ...messages]),
7285
+ [messages]
7286
+ );
6890
7287
  const contextPct = useMemo(() => {
6891
7288
  const window = contextWindow(model);
6892
7289
  if (!window) return 0;
6893
- const used2 = conversationTokens([
6894
- { role: "system", content: contextRef.current.systemPrompt },
6895
- ...messages
6896
- ]);
6897
- return used2 / window;
6898
- }, [contextWindow, messages, model]);
7290
+ return conversationUsed / window;
7291
+ }, [contextWindow, conversationUsed, model]);
7292
+ const schemaTokens = useMemo(
7293
+ () => estimateTokens(
7294
+ JSON.stringify(toolSchemaFor(mode, mcpRef.current.toolSchemas(), { webSearch: searchRef.current }))
7295
+ ),
7296
+ [mode]
7297
+ );
7298
+ const typed = toText(editor);
7299
+ let estimate;
7300
+ const info = catalogRef.current.find((m) => m.id === model);
7301
+ const shape = client.taskShape;
7302
+ if (!busy && info && shape && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7303
+ const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7304
+ const onFree = Boolean(info.free && freeTier?.models.includes(model));
7305
+ const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
7306
+ estimate = estimateClause(range, verdict, !onFree && (freeTier?.credits_left ?? 0) > 0);
7307
+ }
6899
7308
  const modeText = ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}`;
6900
7309
  const statusText = fitRow(
6901
7310
  ` ${statusLine(
@@ -6908,12 +7317,19 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6908
7317
  // Only while a free model is selected: on a premium one the allowance
6909
7318
  // is real but irrelevant, and a countdown that has nothing to do with
6910
7319
  // the next turn is a number that teaches people to stop reading the row.
6911
- ...freeTier?.models.includes(model) ? { freeLeft: freeTier.credits_left } : {}
7320
+ ...freeTier?.models.includes(model) ? { freeLeft: freeTier.credits_left } : {},
7321
+ ...estimate ? { estimate: estimate.variants } : {}
6912
7322
  },
6913
7323
  Math.max(1, cols - visibleLength(modeText) - 2)
6914
7324
  )}`,
6915
7325
  Math.max(0, cols - visibleLength(modeText))
6916
7326
  );
7327
+ const statusParts = (() => {
7328
+ const shown = estimate?.warn ? estimate.variants.find((v) => statusText.includes(v)) : void 0;
7329
+ if (shown === void 0) return { before: statusText, after: "" };
7330
+ const at = statusText.indexOf(shown);
7331
+ return { before: statusText.slice(0, at), warn: shown, after: statusText.slice(at + shown.length) };
7332
+ })();
6917
7333
  const hintText = quitHint ? fitRow(" press ctrl+c again to quit", cols) : ` ${pickHint(
6918
7334
  ["\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close", "\u2191\u2193 \xB7 \u23CE run \xB7 tab \xB7 esc", "\u2191\u2193 \u23CE tab esc"],
6919
7335
  Math.max(1, cols - 2)
@@ -6923,12 +7339,47 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6923
7339
  const room = cols - visibleLength(head);
6924
7340
  return { key: item.value, selected: i === sel, head, hint: room > 1 ? fitRow(item.hint, room) : "" };
6925
7341
  });
6926
- const busyText = fitRow(`${busyLabel} ${elapsed}s \xB7 esc to stop`, Math.max(1, cols - visibleLength(`${spinner} `)));
7342
+ const busyCols = showCard ? cols - SIDEBAR_CARD_COLS : cols;
7343
+ const busyText = fitRow(
7344
+ `${busyLabel} ${elapsed}s \xB7 esc to stop`,
7345
+ Math.max(1, busyCols - visibleLength(`${spinner} `))
7346
+ );
6927
7347
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
6928
7348
  /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }, staticEpoch),
6929
7349
  spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
6930
7350
  liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
6931
- busyHeight > 0 ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
7351
+ busyHeight > 0 && showCard && card ? (
7352
+ /*
7353
+ The working line with the sidebar card beside it. Two columns, two
7354
+ rows: the working line and the blank row under it on the left, the
7355
+ card on the right. The blank row is the one the block below would
7356
+ otherwise have spent on its margin, so the frame is exactly as tall
7357
+ with the card as without it — the input box does not move when a card
7358
+ arrives or goes, and the budget above never had to count it.
7359
+ */
7360
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
7361
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: busyCols, children: [
7362
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7363
+ /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
7364
+ spinner,
7365
+ " "
7366
+ ] }),
7367
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: busyText })
7368
+ ] }),
7369
+ /* @__PURE__ */ jsx2(Text2, { children: " " })
7370
+ ] }),
7371
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: SIDEBAR_CARD_COLS, children: [
7372
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7373
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: card.rule }),
7374
+ /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_MARK } : { dimColor: true }, wrap: "truncate-end", children: card.label })
7375
+ ] }),
7376
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7377
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: card.rule }),
7378
+ /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_TEXT } : { dimColor: true }, wrap: "truncate-end", children: card.body })
7379
+ ] })
7380
+ ] })
7381
+ ] })
7382
+ ) : busyHeight > 0 ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
6932
7383
  /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
6933
7384
  spinner,
6934
7385
  " "
@@ -6968,20 +7419,9 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6968
7419
  pickerCompact ? null : /* @__PURE__ */ jsx2(Box2, { height: 1 }),
6969
7420
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pickerHint })
6970
7421
  ] }) : null,
6971
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
7422
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showCard ? 0 : 1, children: [
6972
7423
  queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: line2 }, i)),
6973
- showSponsor && sponsorLine ? /* @__PURE__ */ jsxs2(Box2, { children: [
6974
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: sponsorLine.marker }),
6975
- sponsorLine.label ? /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_MARK } : { dimColor: true }, wrap: "truncate-end", children: sponsorLine.label }) : null,
6976
- /* @__PURE__ */ jsx2(
6977
- Text2,
6978
- {
6979
- ...adColor && sponsorLine.origin === "network" ? { color: AD_TEXT } : { dimColor: true },
6980
- wrap: "truncate-end",
6981
- children: sponsorLine.body
6982
- }
6983
- )
6984
- ] }) : null,
7424
+ showSponsor && sponsorLine ? /* @__PURE__ */ jsx2(SponsorRowView, { row: sponsorLine, adColor }) : null,
6985
7425
  inputBorder ? /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO3 : MANGO_BRIGHT, paddingX: 1, children: [
6986
7426
  /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
6987
7427
  /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
@@ -6995,18 +7435,30 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6995
7435
  ] }, row.key)),
6996
7436
  !showStatus ? null : quitHint || menu.length > 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: hintText }) : /* @__PURE__ */ jsxs2(Box2, { children: [
6997
7437
  /* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, wrap: "truncate-end", children: modeText }),
6998
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: statusText })
6999
- ] })
7438
+ statusParts.warn === void 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: statusText }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
7439
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: statusParts.before }),
7440
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: statusParts.warn }),
7441
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: statusParts.after })
7442
+ ] })
7443
+ ] }),
7444
+ showStatusBar && statusBarLine ? /* @__PURE__ */ jsx2(SponsorRowView, { row: statusBarLine, adColor }) : null
7000
7445
  ] })
7001
7446
  ] });
7002
7447
  }
7448
+ function SponsorRowView({ row, adColor }) {
7449
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
7450
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: row.marker }),
7451
+ row.label ? /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_MARK } : { dimColor: true }, wrap: "truncate-end", children: row.label }) : null,
7452
+ /* @__PURE__ */ jsx2(Text2, { ...adColor && row.origin === "network" ? { color: AD_TEXT } : { dimColor: true }, wrap: "truncate-end", children: row.body })
7453
+ ] });
7454
+ }
7003
7455
  function sleep4(ms, signal) {
7004
- return new Promise((resolve2) => {
7005
- if (signal.aborted) return resolve2();
7456
+ return new Promise((resolve3) => {
7457
+ if (signal.aborted) return resolve3();
7006
7458
  const done = () => {
7007
7459
  clearTimeout(timer);
7008
7460
  signal.removeEventListener("abort", done);
7009
- resolve2();
7461
+ resolve3();
7010
7462
  };
7011
7463
  const timer = setTimeout(done, ms);
7012
7464
  signal.addEventListener("abort", done, { once: true });
@@ -7030,9 +7482,9 @@ function renderInput(state, from, rows) {
7030
7482
  function modelHint(m, free) {
7031
7483
  const effort = adsPerTaskLabel(m.est_ads_per_task);
7032
7484
  if (!m.free || !free?.models.includes(m.id)) return effort;
7033
- const turns = free.turns_left[m.id] ?? 0;
7034
- if (turns <= 0) return `free: none left today \xB7 ${effort}`;
7035
- return `free: ~${turns} turn${turns === 1 ? "" : "s"} left \xB7 ${effort}`;
7485
+ const turns2 = free.turns_left[m.id] ?? 0;
7486
+ if (turns2 <= 0) return `free: none left today \xB7 ${effort}`;
7487
+ return `free: ~${turns2} turn${turns2 === 1 ? "" : "s"} left \xB7 ${effort}`;
7036
7488
  }
7037
7489
  function turnMeta(result) {
7038
7490
  const n = (v) => v.toLocaleString("en-US");
@@ -7045,6 +7497,14 @@ function turnMeta(result) {
7045
7497
  return `[free \xB7 ${n(result.creditsWaived)} credits waived${left}]`;
7046
7498
  }
7047
7499
  function statusLine(o, cols) {
7500
+ const phrasings = o.estimate === void 0 ? [] : typeof o.estimate === "string" ? [o.estimate] : [...o.estimate];
7501
+ for (const estimate of [...phrasings, void 0]) {
7502
+ const line2 = statusRow(o, estimate, cols);
7503
+ if (estimate === void 0 || visibleLength(line2) <= cols) return fitRow(line2, cols);
7504
+ }
7505
+ return fitRow(statusRow(o, void 0, cols), cols);
7506
+ }
7507
+ function statusRow(o, estimate, cols) {
7048
7508
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
7049
7509
  const clauses = [
7050
7510
  { text: o.model, rank: KEEP_ALWAYS },
@@ -7056,6 +7516,7 @@ function statusLine(o, cols) {
7056
7516
  text: o.freeLeft === void 0 ? void 0 : `free ${o.freeLeft.toLocaleString("en-US")}`,
7057
7517
  rank: 4
7058
7518
  },
7519
+ { text: estimate, rank: 5 },
7059
7520
  { text: o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0, rank: 2 },
7060
7521
  { text: burn, rank: 1 },
7061
7522
  {
@@ -7067,7 +7528,7 @@ function statusLine(o, cols) {
7067
7528
  const join7 = (minRank) => clauses.filter((c2) => c2.rank >= minRank).map((c2) => c2.text).join(" \xB7 ");
7068
7529
  let line2 = join7(0);
7069
7530
  for (let minRank = 1; minRank <= 5 && visibleLength(line2) > cols; minRank++) line2 = join7(minRank);
7070
- return fitRow(line2, cols);
7531
+ return line2;
7071
7532
  }
7072
7533
  function windowStart(sel, total, size) {
7073
7534
  if (total <= size) return 0;
@@ -7118,7 +7579,7 @@ function tailLines(text, max) {
7118
7579
  const lines = text.split("\n");
7119
7580
  return lines.length <= max ? text : lines.slice(-max).join("\n");
7120
7581
  }
7121
- var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, WAITING_FOR_PAYMENT, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, BUY_CHROME, BUY_CHROME_COMPACT, ASK_HINT, PLAN_HINT, BUY_HINT, BUY_CONFIRM_WORD, BUY_TYPED_MAX, BUY_TOO_SHORT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
7582
+ var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, WAITING_FOR_PAYMENT, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, BUY_CHROME, BUY_CHROME_COMPACT, ASK_HINT, PLAN_HINT, BUY_HINT, BUY_CONFIRM_WORD, BUY_TYPED_MAX, BUY_TOO_SHORT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, INIT_HINT_AFTER_TURNS, EARN_ROW, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
7122
7583
  var init_app = __esm({
7123
7584
  "src/tui/app.tsx"() {
7124
7585
  "use strict";
@@ -7133,6 +7594,8 @@ var init_app = __esm({
7133
7594
  init_context();
7134
7595
  init_mentions();
7135
7596
  init_compact();
7597
+ init_estimate();
7598
+ init_free_tier();
7136
7599
  init_kimi();
7137
7600
  init_browser();
7138
7601
  init_version();
@@ -7143,6 +7606,7 @@ var init_app = __esm({
7143
7606
  init_counter();
7144
7607
  init_provider();
7145
7608
  init_network();
7609
+ init_formats();
7146
7610
  init_render();
7147
7611
  init_commands();
7148
7612
  init_editor();
@@ -7195,6 +7659,8 @@ var init_app = __esm({
7195
7659
  EXPAND_MAX_LINES = 400;
7196
7660
  FALLBACK_ROWS = 24;
7197
7661
  FALLBACK_COLS = 80;
7662
+ INIT_HINT_AFTER_TURNS = 3;
7663
+ EARN_ROW = "\0earn";
7198
7664
  CONTEXT_NOTICE_AT = 0.6;
7199
7665
  KEEP_ALWAYS = Number.POSITIVE_INFINITY;
7200
7666
  }
@@ -7228,6 +7694,7 @@ var init_tui = __esm({
7228
7694
  init_client();
7229
7695
  init_config();
7230
7696
  init_login();
7697
+ init_free_tier();
7231
7698
  init_agent();
7232
7699
  init_tools();
7233
7700
  init_context();
@@ -7366,6 +7833,7 @@ async function login(client, config, args) {
7366
7833
  \u2713 logged in as ${who}`) + suffix);
7367
7834
  const bonus = account.created ? c.dim(account.signupBonus === void 0 ? " (signup bonus)" : ` (signup bonus ${credits(account.signupBonus)})`) : "";
7368
7835
  console.log(` balance: ${c.bold(credits(account.balance))} credits${bonus}`);
7836
+ if (account.model) console.log(` model: ${c.bold(account.model)} ${c.dim("(the default for new accounts)")}`);
7369
7837
  await showStreak(client);
7370
7838
  console.log(c.dim(` token stored in ${configPath()}`));
7371
7839
  return;
@@ -7429,8 +7897,8 @@ async function listModels(client) {
7429
7897
  console.log(c.bold("Models (credit prices per 1M tokens)"));
7430
7898
  for (const m of models) {
7431
7899
  const tag = m.ad_fundable ? "" : c.dim(" [paid/BYOK]");
7432
- const turns = free?.models.includes(m.id) ? free.turns_left[m.id] ?? 0 : void 0;
7433
- const freeTag = !m.free ? "" : turns === void 0 ? c.green(" [free]") : turns > 0 ? c.green(` [free: ~${turns} turn${turns === 1 ? "" : "s"} left today]`) : c.dim(" [free: none left today]");
7900
+ const turns2 = free?.models.includes(m.id) ? free.turns_left[m.id] ?? 0 : void 0;
7901
+ const freeTag = !m.free ? "" : turns2 === void 0 ? c.green(" [free]") : turns2 > 0 ? c.green(` [free: ~${turns2} turn${turns2 === 1 ? "" : "s"} left today]`) : c.dim(" [free: none left today]");
7434
7902
  console.log(
7435
7903
  ` ${c.cyan(m.id.padEnd(22))} ${m.tier.padEnd(8)} in ${String(m.credits_per_mtoken_input).padStart(9)} out ${String(m.credits_per_mtoken_output).padStart(9)} ${adsPerTaskLabel(m.est_ads_per_task)}${tag}${freeTag}`
7436
7904
  );
@@ -7584,6 +8052,8 @@ ${NO_SEARCH_INSTRUCTION}`),
7584
8052
  if (err instanceof PaywallError) {
7585
8053
  console.log(c.yellow(`
7586
8054
  Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8055
+ const freeLine = freeAlternativeLine(err.freeModels);
8056
+ if (freeLine) console.log(c.green(freeLine));
7587
8057
  console.log(
7588
8058
  c.yellow(
7589
8059
  `Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantLabel(err.grantPerAd)}.`
@@ -7636,6 +8106,10 @@ async function codeCmd(client, config, rest) {
7636
8106
  );
7637
8107
  if (w.balance <= 0) {
7638
8108
  console.log(c.yellow(" You're out of credits \u2014 run `clixad earn` first, or the agent will hit a paywall."));
8109
+ const pick = w.free_tier && w.free_tier.credits_left > 0 ? freeModelToName(w.free_tier, config.model) : void 0;
8110
+ if (pick && pick.id !== config.model) {
8111
+ console.log(c.green(` ${freeAlternativeLine([{ id: pick.id, turnsLeft: pick.turns }], { rerun: false })}`));
8112
+ }
7639
8113
  }
7640
8114
  }
7641
8115
  console.log(c.dim(" launching Kimi CLI (Clixad-configured)\u2026\n"));
@@ -7677,6 +8151,8 @@ async function repl(client, config, opts = {}) {
7677
8151
  }
7678
8152
  const streak = await client.claimStreak();
7679
8153
  const wallet = await safeWallet(client);
8154
+ const cwd = process.cwd();
8155
+ const initHint = shouldSuggestInit(cwd) ? { returning: hasSessionIn(cwd) } : void 0;
7680
8156
  clearScreen();
7681
8157
  const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
7682
8158
  await startTui2({
@@ -7686,7 +8162,8 @@ async function repl(client, config, opts = {}) {
7686
8162
  streak,
7687
8163
  session,
7688
8164
  initialTask: opts.task,
7689
- ...opts.notices?.length ? { notices: opts.notices } : {}
8165
+ ...opts.notices?.length ? { notices: opts.notices } : {},
8166
+ ...initHint ? { initHint } : {}
7690
8167
  });
7691
8168
  }
7692
8169
  async function runTurn(client, config, messages) {
@@ -7702,6 +8179,8 @@ async function runTurn(client, config, messages) {
7702
8179
  process.stdout.write("\n");
7703
8180
  if (err instanceof PaywallError) {
7704
8181
  console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8182
+ const freeLine = freeAlternativeLine(err.freeModels);
8183
+ if (freeLine) console.log(c.green(freeLine));
7705
8184
  console.log(
7706
8185
  c.yellow(`Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantLabel(err.grantPerAd)}.`)
7707
8186
  );
@@ -7735,7 +8214,7 @@ function printHelp() {
7735
8214
  ${c.cyan("login")} [email] create/attach an account (${c.dim("--invite <code>")} if you have one)
7736
8215
  ${c.cyan("logout")} forget the stored token
7737
8216
  ${c.cyan("whoami")} show account + balance
7738
- ${c.cyan("models")} list models with credit prices
8217
+ ${c.cyan("models")} list models with credit prices, and which are free today
7739
8218
  ${c.cyan("model")} <id> set default model
7740
8219
  ${c.cyan("wallet")} balance, ads today, ledger
7741
8220
  ${c.cyan("earn")} open the ad wall to earn credits