clixad 0.0.1-beta.31 → 0.0.1-beta.32

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 (2) hide show
  1. package/dist/clixad.mjs +226 -34
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -324,6 +324,13 @@ function usd(v) {
324
324
  function packsFromClause(cheapestUsd) {
325
325
  return typeof cheapestUsd === "number" && cheapestUsd > 0 ? `, from ${usd(cheapestUsd)}` : "";
326
326
  }
327
+ function parseSpendable(raw) {
328
+ if (!raw || typeof raw !== "object") return void 0;
329
+ const r = raw;
330
+ const ok = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
331
+ if (!ok(r.budget) || !ok(r.premium) || !ok(r.budget_after_reset)) return void 0;
332
+ return { budget: r.budget, premium: r.premium, budgetAfterReset: r.budget_after_reset };
333
+ }
327
334
  function usableStreak(streak) {
328
335
  if (typeof streak !== "object" || streak === null) return void 0;
329
336
  const s = streak;
@@ -481,7 +488,11 @@ function asSignupClosed(status, data) {
481
488
  const d = data;
482
489
  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;
483
490
  }
484
- function premiumLockedError(message, model, body) {
491
+ function spendableAt(raw, balance) {
492
+ const figures = parseSpendable(raw);
493
+ return figures && typeof balance === "number" && Number.isFinite(balance) ? { figures, balance } : void 0;
494
+ }
495
+ function premiumLockedError(message, model, body, spendable) {
485
496
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
486
497
  return new PremiumLockedError(
487
498
  message,
@@ -491,7 +502,8 @@ function premiumLockedError(message, model, body) {
491
502
  typeof body?.unlock_usd === "number" && body.unlock_usd > 0 ? body.unlock_usd : null,
492
503
  num(body?.grant_per_ad),
493
504
  freeAlternatives(body?.free_models),
494
- usableStreak(body?.streak)
505
+ usableStreak(body?.streak),
506
+ spendable
495
507
  );
496
508
  }
497
509
  function shortfallHeadline(err, model) {
@@ -516,9 +528,23 @@ function freeAlternatives(list) {
516
528
  }
517
529
  async function refusal402(res, model) {
518
530
  const body = await res.json();
519
- if (body.error === "premium_locked") return premiumLockedError(body.message ?? "", model, body.premium);
531
+ if (body.error === "premium_locked") {
532
+ return premiumLockedError(body.message ?? "", model, body.premium, spendableAt(body.spendable, body.balance));
533
+ }
520
534
  return paywallError(body);
521
535
  }
536
+ async function refusal429(res) {
537
+ const text = await res.text().catch(() => "");
538
+ try {
539
+ const body = JSON.parse(text);
540
+ if (typeof body.error === "string" && LIMIT_REFUSALS.has(body.error) && typeof body.message === "string") {
541
+ return new LimitRefusalError(body.message, body.error, spendableAt(body.spendable, body.balance));
542
+ }
543
+ } catch {
544
+ }
545
+ const detail = safeDetail(text);
546
+ return new Error(detail ? `chat failed: 429 \u2014 ${detail}` : "chat failed: 429");
547
+ }
522
548
  function neverConnected(code) {
523
549
  return code !== void 0 && NEVER_CONNECTED.has(code);
524
550
  }
@@ -643,7 +669,7 @@ async function* parseSSE(body) {
643
669
  }
644
670
  }
645
671
  }
646
- var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, PremiumLockedError, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
672
+ var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, PremiumLockedError, LimitRefusalError, LIMIT_REFUSALS, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
647
673
  var init_client = __esm({
648
674
  "src/client.ts"() {
649
675
  "use strict";
@@ -696,7 +722,7 @@ var init_client = __esm({
696
722
  cheapestPackUsd;
697
723
  };
698
724
  PremiumLockedError = class extends Error {
699
- constructor(message, model, neededCredits, premiumCredits, unlockUsd, grantPerAd, freeModels, streak) {
725
+ constructor(message, model, neededCredits, premiumCredits, unlockUsd, grantPerAd, freeModels, streak, spendable) {
700
726
  super(message);
701
727
  this.model = model;
702
728
  this.neededCredits = neededCredits;
@@ -705,6 +731,7 @@ var init_client = __esm({
705
731
  this.grantPerAd = grantPerAd;
706
732
  this.freeModels = freeModels;
707
733
  this.streak = streak;
734
+ this.spendable = spendable;
708
735
  this.name = "PremiumLockedError";
709
736
  }
710
737
  model;
@@ -714,7 +741,19 @@ var init_client = __esm({
714
741
  grantPerAd;
715
742
  freeModels;
716
743
  streak;
744
+ spendable;
717
745
  };
746
+ LimitRefusalError = class extends Error {
747
+ constructor(message, type, spendable) {
748
+ super(message);
749
+ this.type = type;
750
+ this.spendable = spendable;
751
+ this.name = "LimitRefusalError";
752
+ }
753
+ type;
754
+ spendable;
755
+ };
756
+ LIMIT_REFUSALS = /* @__PURE__ */ new Set(["paid_user_limit_reached", "paid_budget_spent", "paid_budget_unavailable"]);
718
757
  GatewayUnreachableError = class extends Error {
719
758
  constructor(host, code) {
720
759
  const where = `${host}${code ? ` (${code})` : ""}`;
@@ -1413,16 +1452,19 @@ var init_client = __esm({
1413
1452
  );
1414
1453
  if (res.status === 401) throw await authFailure(res, "chat");
1415
1454
  if (res.status === 402) throw await refusal402(res, model);
1455
+ if (res.status === 429) throw await refusal429(res);
1416
1456
  if (!res.ok) throw new Error(await httpFailure(res, "chat"));
1417
1457
  const data = await res.json();
1418
1458
  const choice = data.choices[0];
1419
1459
  if (!choice) throw new Error("chat: no choices in response");
1460
+ const spendable = parseSpendable(data.clixad.spendable);
1420
1461
  return {
1421
1462
  message: choice.message,
1422
1463
  finishReason: choice.finish_reason,
1423
1464
  creditsCharged: data.clixad.credits_charged,
1424
1465
  balance: data.clixad.balance,
1425
- ...freeMeta(data.clixad)
1466
+ ...freeMeta(data.clixad),
1467
+ ...spendable ? { spendable } : {}
1426
1468
  };
1427
1469
  }
1428
1470
  /**
@@ -1451,6 +1493,8 @@ var init_client = __esm({
1451
1493
  let free = {};
1452
1494
  let balance = 0;
1453
1495
  let streamError;
1496
+ let streamErrorType;
1497
+ let spendable;
1454
1498
  let premiumRefusal;
1455
1499
  const calls = /* @__PURE__ */ new Map();
1456
1500
  const reasoning = [];
@@ -1473,24 +1517,36 @@ var init_client = __esm({
1473
1517
  }
1474
1518
  mergeReasoningDetails(reasoning, choice?.delta?.reasoning_details);
1475
1519
  if (choice?.finish_reason) finishReason = choice.finish_reason;
1476
- if (json.error) streamError = json.error.message || "the model stream failed";
1520
+ if (json.error) {
1521
+ streamError = json.error.message || "the model stream failed";
1522
+ streamErrorType = json.error.type;
1523
+ }
1477
1524
  if (json.error?.type === "premium_locked") {
1478
- premiumRefusal = premiumLockedError(streamError ?? "", model, json.clixad?.premium);
1525
+ premiumRefusal = premiumLockedError(
1526
+ streamError ?? "",
1527
+ model,
1528
+ json.clixad?.premium,
1529
+ spendableAt(json.clixad?.spendable, json.clixad?.balance)
1530
+ );
1479
1531
  }
1480
1532
  if (json.clixad) {
1481
1533
  creditsCharged = json.clixad.credits_charged;
1482
1534
  balance = json.clixad.balance;
1483
1535
  free = freeMeta(json.clixad);
1536
+ spendable = parseSpendable(json.clixad.spendable);
1484
1537
  }
1485
1538
  }
1486
1539
  if (premiumRefusal) throw premiumRefusal;
1540
+ if (streamError && streamErrorType && LIMIT_REFUSALS.has(streamErrorType)) {
1541
+ throw new LimitRefusalError(streamError, streamErrorType, spendable ? { figures: spendable, balance } : void 0);
1542
+ }
1487
1543
  if (streamError) throw new Error(streamError);
1488
1544
  const message = { role: "assistant", content: content || null };
1489
1545
  if (calls.size > 0) {
1490
1546
  message.tool_calls = [...calls.entries()].sort(([a], [b]) => a - b).map(([, c2]) => c2);
1491
1547
  if (reasoning.length) message.reasoning_details = reasoning;
1492
1548
  }
1493
- return { message, finishReason, creditsCharged, balance, ...free };
1549
+ return { message, finishReason, creditsCharged, balance, ...free, ...spendable ? { spendable } : {} };
1494
1550
  }
1495
1551
  };
1496
1552
  }
@@ -4460,7 +4516,8 @@ ${NO_SEARCH_INSTRUCTION}`,
4460
4516
  free: true,
4461
4517
  creditsWaived: turn.creditsWaived ?? 0,
4462
4518
  freeCreditsLeft: turn.freeCreditsLeft ?? 0
4463
- } : {}
4519
+ } : {},
4520
+ ...turn.spendable ? { spendable: turn.spendable } : {}
4464
4521
  });
4465
4522
  }
4466
4523
  let retriedEmpty = false;
@@ -5032,12 +5089,12 @@ var init_kimi = __esm({
5032
5089
  CONTEXT_TOKENS = {
5033
5090
  "gpt-5-nano": 4e5,
5034
5091
  "gemini-2.5-flash-lite": 1048576,
5092
+ "claude-haiku-5.5": 1e6,
5035
5093
  "deepseek-v4-flash": 1048576,
5036
5094
  "gemini-3.5-flash-lite": 1048576,
5037
5095
  "kimi-k2.5": 262144,
5038
5096
  "gemini-3.8-flash": 1048576,
5039
5097
  "kimi-k2.7-code": 262144,
5040
- "claude-haiku-4.5": 2e5,
5041
5098
  "claude-sonnet-5.5": 1e6,
5042
5099
  "gpt-6-sol": 105e4,
5043
5100
  "kimi-k3": 1048576,
@@ -5047,14 +5104,17 @@ var init_kimi = __esm({
5047
5104
  // packages/pricing), so an install still configured with one compacts
5048
5105
  // against the window of the model that actually answers.
5049
5106
  "gemini-3.1-flash-lite": 1048576,
5050
- "kimi-k2": 262144,
5107
+ // Served by Claude Haiku 5.5 since 2026-10-12, when Kimi K2.5 left the free set.
5108
+ "kimi-k2": 1e6,
5051
5109
  "gemini-3.6-flash": 1048576,
5052
5110
  "gpt-5.2": 105e4,
5053
5111
  "claude-opus-5": 1e6,
5054
5112
  "gpt-5.5": 105e4,
5055
5113
  "gpt-5.2-pro": 105e4,
5056
5114
  // Retired on 2026-09-29 for Claude Sonnet 5.5, same window.
5057
- "claude-sonnet-5": 1e6
5115
+ "claude-sonnet-5": 1e6,
5116
+ // Retired on 2026-10-12 for Claude Haiku 5.5.
5117
+ "claude-haiku-4.5": 1e6
5058
5118
  };
5059
5119
  DEFAULT_CONTEXT_TOKENS = 256e3;
5060
5120
  INSTALL_HELP = `Kimi CLI is not installed (or not on PATH).
@@ -5493,6 +5553,7 @@ ${summary}` },
5493
5553
  summary,
5494
5554
  creditsCharged: turn.creditsCharged,
5495
5555
  balance: turn.balance,
5556
+ ...turn.spendable ? { spendable: turn.spendable } : {},
5496
5557
  before,
5497
5558
  after: conversationTokens(compacted)
5498
5559
  };
@@ -6182,12 +6243,22 @@ function headRows(text, cols, max) {
6182
6243
  if (rows.length <= max) return text;
6183
6244
  return rows.slice(0, max).join("\n");
6184
6245
  }
6185
- function errorNotice(message, rows = ERROR_NOTICE_ROWS) {
6246
+ function errorNotice(message, rows = ERROR_NOTICE_ROWS, indent = "") {
6186
6247
  const clean = message.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)?/g, " ").replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g, " ");
6187
6248
  const lines = clean.split("\n");
6188
- const kept = lines.slice(0, rows).map((l) => l.length > ERROR_NOTICE_COLS ? `${l.slice(0, ERROR_NOTICE_COLS - 1)}\u2026` : l);
6189
- if (lines.length > rows) kept.push(` \u2026 +${lines.length - rows} more lines`);
6190
- return kept.join("\n");
6249
+ const kept = [];
6250
+ let left = ERROR_NOTICE_CHARS;
6251
+ for (const line2 of lines.slice(0, rows)) {
6252
+ if (line2.length > left) {
6253
+ kept.push(`${line2.slice(0, Math.max(0, left - 1))}\u2026`);
6254
+ break;
6255
+ }
6256
+ kept.push(line2);
6257
+ left -= line2.length;
6258
+ }
6259
+ if (lines.length > kept.length) kept.push(`\u2026 +${lines.length - kept.length} more lines`);
6260
+ return kept.join(`
6261
+ ${indent}`);
6191
6262
  }
6192
6263
  function entryHeight(entry, cols) {
6193
6264
  const margin = entry.kind === "banner" ? 0 : 1;
@@ -6235,7 +6306,7 @@ function EntryView({ entry }) {
6235
6306
  ) });
6236
6307
  }
6237
6308
  }
6238
- var MANGO3, MANGO_BRIGHT, SLATE, AD_TEXT, AD_MARK, MODE_STYLE, WRAP_LIKE_INK, DIALOG_CHROME_COLS, ERROR_NOTICE_ROWS, ERROR_NOTICE_COLS;
6309
+ var MANGO3, MANGO_BRIGHT, SLATE, AD_TEXT, AD_MARK, MODE_STYLE, WRAP_LIKE_INK, DIALOG_CHROME_COLS, ERROR_NOTICE_ROWS, ERROR_NOTICE_CHARS;
6239
6310
  var init_views = __esm({
6240
6311
  "src/tui/views.tsx"() {
6241
6312
  "use strict";
@@ -6254,7 +6325,7 @@ var init_views = __esm({
6254
6325
  WRAP_LIKE_INK = { trim: false, hard: true };
6255
6326
  DIALOG_CHROME_COLS = 4;
6256
6327
  ERROR_NOTICE_ROWS = 12;
6257
- ERROR_NOTICE_COLS = 200;
6328
+ ERROR_NOTICE_CHARS = 2400;
6258
6329
  }
6259
6330
  });
6260
6331
 
@@ -6279,6 +6350,9 @@ function buyConfirmLines(pack) {
6279
6350
  `Terms: ${TERMS_URL}`
6280
6351
  ].join("\n");
6281
6352
  }
6353
+ function utcDay(t = Date.now()) {
6354
+ return new Date(t).toISOString().slice(0, 10);
6355
+ }
6282
6356
  function useTerminalSize() {
6283
6357
  const { stdout } = useStdout();
6284
6358
  const snapshot2 = useRef({
@@ -6402,6 +6476,7 @@ function App({
6402
6476
  const runStartedAtRef = useRef(Date.now());
6403
6477
  const contextRef = useRef(collectContext(root));
6404
6478
  const catalogRef = useRef([]);
6479
+ const [, setCatalogArrived] = useState(0);
6405
6480
  const searchRef = useRef(false);
6406
6481
  const outputsRef = useRef([]);
6407
6482
  const expandedRef = useRef(/* @__PURE__ */ new Set());
@@ -6423,6 +6498,52 @@ function App({
6423
6498
  const refreshPremium = useCallback(() => {
6424
6499
  void client.wallet(void 0, { premium: true }).then((w) => setPremium(w.premium)).catch(() => void 0);
6425
6500
  }, [client, setPremium]);
6501
+ const initialSpendable = useMemo(() => {
6502
+ const figures = parseSpendable(wallet?.spendable);
6503
+ return figures && wallet ? { figures, balance: wallet.balance, day: utcDay() } : void 0;
6504
+ }, []);
6505
+ const [spendable, setSpendableState] = useState(initialSpendable);
6506
+ const spendableRef = useRef(initialSpendable);
6507
+ const takeSpendable = useCallback(
6508
+ (figures, balance2) => {
6509
+ const next = { figures, balance: balance2, day: utcDay() };
6510
+ spendableRef.current = next;
6511
+ setSpendableState(next);
6512
+ setBalance(balance2);
6513
+ const p = premiumRef.current;
6514
+ if (p) setPremium({ ...p, credits: figures.premium });
6515
+ },
6516
+ [setPremium]
6517
+ );
6518
+ const balanceSeqRef = useRef(0);
6519
+ const spendableInFlightRef = useRef(false);
6520
+ const spendableAskedRef = useRef(null);
6521
+ const refreshSpendable = useCallback(() => {
6522
+ if (spendableInFlightRef.current) return;
6523
+ spendableInFlightRef.current = true;
6524
+ const seq = balanceSeqRef.current;
6525
+ void client.wallet(void 0, { premium: true }).then((w) => {
6526
+ if (balanceSeqRef.current !== seq) return;
6527
+ const figures = parseSpendable(w.spendable);
6528
+ if (figures) takeSpendable(figures, w.balance);
6529
+ else setBalance(w.balance);
6530
+ if (w.premium) setPremium(w.premium);
6531
+ }).catch(() => void 0).finally(() => {
6532
+ spendableInFlightRef.current = false;
6533
+ });
6534
+ }, [client, setPremium, takeSpendable]);
6535
+ const [dayTick, setDayTick] = useState(0);
6536
+ useEffect(() => {
6537
+ const now = Date.now();
6538
+ const next = Date.UTC(
6539
+ new Date(now).getUTCFullYear(),
6540
+ new Date(now).getUTCMonth(),
6541
+ new Date(now).getUTCDate() + 1
6542
+ );
6543
+ const timer = setTimeout(() => setDayTick((t) => t + 1), next - now + 1e3);
6544
+ timer.unref?.();
6545
+ return () => clearTimeout(timer);
6546
+ }, [dayTick]);
6426
6547
  const mcpRef = useRef(McpHub.empty());
6427
6548
  const todosRef = useRef(new TodoList());
6428
6549
  const lastTurnRef = useRef(null);
@@ -6577,6 +6698,7 @@ function App({
6577
6698
  }
6578
6699
  client.models().then((m) => {
6579
6700
  catalogRef.current = m;
6701
+ setCatalogArrived((n) => n + 1);
6580
6702
  searchRef.current = client.capabilities?.web_search === true;
6581
6703
  const successor = client.retiredModels[modelRef.current];
6582
6704
  if (successor) {
@@ -6841,8 +6963,11 @@ function App({
6841
6963
  push({ kind: "notice", text: ` showing the model: ${event.labels.join(", ")}` });
6842
6964
  return;
6843
6965
  case "usage":
6966
+ balanceSeqRef.current++;
6844
6967
  setBalance(event.balance);
6845
- if (event.creditsCharged > 0 && premiumRef.current) {
6968
+ if (event.spendable) {
6969
+ takeSpendable(event.spendable, event.balance);
6970
+ } else if (event.creditsCharged > 0 && premiumRef.current) {
6846
6971
  const p = premiumRef.current;
6847
6972
  setPremium({
6848
6973
  ...p,
@@ -6860,8 +6985,18 @@ function App({
6860
6985
  return;
6861
6986
  }
6862
6987
  },
6863
- [cols, flushDelta, push, setPremium]
6988
+ [cols, flushDelta, push, setPremium, takeSpendable]
6864
6989
  );
6990
+ useEffect(() => {
6991
+ if (balance === void 0) return;
6992
+ const s = spendableRef.current;
6993
+ const day = utcDay();
6994
+ if (s && s.balance === balance && s.day === day) return;
6995
+ const key = `${balance}|${day}`;
6996
+ if (spendableAskedRef.current === key) return;
6997
+ spendableAskedRef.current = key;
6998
+ refreshSpendable();
6999
+ }, [balance, spendable, dayTick, refreshSpendable]);
6865
7000
  const runTurn2 = useCallback(
6866
7001
  async (task, images = []) => {
6867
7002
  pendingFreeRef.current = [];
@@ -6884,7 +7019,9 @@ function App({
6884
7019
  if (res) {
6885
7020
  history = res.messages.slice(1);
6886
7021
  setMessages(history);
7022
+ balanceSeqRef.current++;
6887
7023
  setBalance(res.balance);
7024
+ if (res.spendable) takeSpendable(res.spendable, res.balance);
6888
7025
  setSpent((s) => s + res.creditsCharged);
6889
7026
  push({
6890
7027
  kind: "notice",
@@ -7006,6 +7143,10 @@ ${preferencesText}` : ""),
7006
7143
  }
7007
7144
  }
7008
7145
  } catch (err) {
7146
+ if ((err instanceof LimitRefusalError || err instanceof PremiumLockedError) && err.spendable) {
7147
+ balanceSeqRef.current++;
7148
+ takeSpendable(err.spendable.figures, err.spendable.balance);
7149
+ }
7009
7150
  if (err instanceof PremiumLockedError) {
7010
7151
  pendingTaskRef.current = { task, images };
7011
7152
  pendingPremiumRef.current = err;
@@ -7055,7 +7196,7 @@ ${preferencesText}` : ""),
7055
7196
  } else if (err instanceof AuthError) {
7056
7197
  push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
7057
7198
  } else {
7058
- push({ kind: "notice", tone: "error", text: ` error: ${errorNotice(err.message)}` });
7199
+ push({ kind: "notice", tone: "error", text: ` error: ${errorNotice(err.message, void 0, " ")}` });
7059
7200
  }
7060
7201
  } finally {
7061
7202
  abortRef.current = null;
@@ -7080,7 +7221,8 @@ ${preferencesText}` : ""),
7080
7221
  nextSponsor,
7081
7222
  permit,
7082
7223
  push,
7083
- root
7224
+ root,
7225
+ takeSpendable
7084
7226
  ]
7085
7227
  );
7086
7228
  const stopCurrent = useCallback(() => {
@@ -7099,7 +7241,7 @@ ${preferencesText}` : ""),
7099
7241
  } catch (err) {
7100
7242
  if (ac.signal.aborted) push({ kind: "notice", tone: "warn", text: " (stopped)" });
7101
7243
  else if (err instanceof AuthError) push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
7102
- else push({ kind: "notice", tone: "error", text: ` ${errorNotice(err.message)}` });
7244
+ else push({ kind: "notice", tone: "error", text: ` ${errorNotice(err.message, void 0, " ")}` });
7103
7245
  } finally {
7104
7246
  busyAbortRef.current = null;
7105
7247
  setBusyLabel(WORKING);
@@ -7592,8 +7734,13 @@ ${preferencesText}` : ""),
7592
7734
  case "wallet":
7593
7735
  await runBusy(LOADING, async (signal) => {
7594
7736
  const streakNow = await client.claimStreak(signal);
7595
- const w = await client.wallet(signal);
7596
- setBalance(w.balance);
7737
+ const w = await client.wallet(signal, { premium: true });
7738
+ const figures = parseSpendable(w.spendable);
7739
+ balanceSeqRef.current++;
7740
+ if (figures) takeSpendable(figures, w.balance);
7741
+ else setBalance(w.balance);
7742
+ if (w.premium) setPremium(w.premium);
7743
+ const usableNow = figures ? walletUsableLines(w.balance, figures) : [];
7597
7744
  const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
7598
7745
  const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
7599
7746
  const screenouts = w.screenouts_today ?? 0;
@@ -7601,7 +7748,8 @@ ${preferencesText}` : ""),
7601
7748
  const streakLine = streakNow && streakLabel(streakNow);
7602
7749
  push({
7603
7750
  kind: "notice",
7604
- text: ` balance ${w.balance.toLocaleString("en-US")} credits \xB7 ${w.ads_today} offers today \xB7 $${w.earned_usd_today.toFixed(2)}/$${w.max_reward_usd_per_day.toFixed(2)} earned` + (screenouts ? `
7751
+ text: ` balance ${w.balance.toLocaleString("en-US")} credits \xB7 ${w.ads_today} offers today \xB7 $${w.earned_usd_today.toFixed(2)}/$${w.max_reward_usd_per_day.toFixed(2)} earned` + usableNow.map((l) => `
7752
+ ${l}`).join("") + (screenouts ? `
7605
7753
  plus ${screenoutCredits.toLocaleString("en-US")} credits from ${screenouts} screenout bonus${screenouts === 1 ? "" : "es"} \u2014 attempts that didn't qualify, which CPX pays a little for anyway.` : "") + (streakLine ? `
7606
7754
  ${streakLine}` : "") + (reversals.length ? `
7607
7755
  recently: ${reversed.toLocaleString("en-US")} credits from ${reversals.length} offer${reversals.length === 1 ? "" : "s"} were reversed by the provider. Run \`clixad wallet\` for the full ledger.` : "")
@@ -8256,14 +8404,25 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
8256
8404
  const typed = toText(editor);
8257
8405
  let estimate;
8258
8406
  const info = catalogRef.current.find((m) => m.id === model);
8407
+ const usable = (() => {
8408
+ const s = spendable;
8409
+ if (!s || balance === void 0 || s.balance !== balance || s.day !== utcDay() || !info?.access) return void 0;
8410
+ const credits2 = Math.max(0, Math.min(balance, info.access === "premium" ? s.figures.premium : s.figures.budget));
8411
+ return {
8412
+ kind: info.access,
8413
+ credits: credits2,
8414
+ locked: Math.max(0, balance - credits2),
8415
+ afterReset: Math.max(credits2, Math.min(balance, s.figures.budgetAfterReset))
8416
+ };
8417
+ })();
8259
8418
  const effortNow = info ? resolveEffort(info, effortBy[model]) : effortBy[model];
8260
8419
  const shape = client.taskShape;
8261
8420
  if (!busy && info && shape && balance !== void 0 && !effortRaised(info, resolveEffort(info, effortBy[model])) && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
8262
8421
  const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
8263
8422
  const onFree = Boolean(info.free && freeTier?.models.includes(model));
8264
- const spendable = info.access === "premium" && premium && !premium.unlocked ? Math.min(balance, premium.credits) : balance;
8423
+ const canSpend = usable ? usable.credits : info.access === "premium" && premium && !premium.unlocked ? Math.min(balance, premium.credits) : balance;
8265
8424
  const verdict = taskVerdict(range, {
8266
- balance: spendable,
8425
+ balance: canSpend,
8267
8426
  ...onFree ? { freeLeft: freeTier.credits_left } : {}
8268
8427
  });
8269
8428
  estimate = estimateClause(range, verdict, !onFree && (freeTier?.credits_left ?? 0) > 0);
@@ -8275,6 +8434,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
8275
8434
  model,
8276
8435
  ...effortNow ? { effort: effortNow } : {},
8277
8436
  balance,
8437
+ ...usable ? { usable } : {},
8278
8438
  spent,
8279
8439
  contextPct,
8280
8440
  minutes: (Date.now() - runStartedAtRef.current) / 6e4,
@@ -8460,15 +8620,41 @@ function turnMeta(result) {
8460
8620
  }
8461
8621
  return `[free \xB7 ${n(result.creditsWaived)} credits waived${left}]`;
8462
8622
  }
8623
+ function lockedPhrasings(u) {
8624
+ if (!u || u.locked <= 0) return [];
8625
+ const n = (v) => v.toLocaleString("en-US");
8626
+ const locked = `${n(u.locked)} bonus locked`;
8627
+ if (u.kind === "premium") return [`${locked} on premium`, locked];
8628
+ const back = u.afterReset - u.credits;
8629
+ return back >= u.locked ? [`${locked} until 00:00 UTC`, locked] : back > 0 ? [`${locked}, ${n(back)} usable from 00:00 UTC`, locked] : [locked];
8630
+ }
8631
+ function walletUsableLines(balance, f) {
8632
+ const n = (v) => Math.max(0, v).toLocaleString("en-US");
8633
+ const budget = Math.min(balance, f.budget);
8634
+ const premium = Math.min(balance, f.premium);
8635
+ if (budget >= balance && premium >= balance) return [];
8636
+ const lines = [`usable now: ${n(budget)} credits on budget models, ${n(premium)} on premium models`];
8637
+ const locked = balance - budget;
8638
+ if (locked > 0) {
8639
+ const back = Math.min(balance, Math.max(budget, f.budgetAfterReset)) - budget;
8640
+ lines.push(
8641
+ back >= locked ? `${n(locked)} bonus credits are over today's limit and become usable at 00:00 UTC` : back > 0 ? `${n(locked)} bonus credits are over today's limit: ${n(back)} become usable at 00:00 UTC, the rest on later days` : `${n(locked)} bonus credits are over today's limit`
8642
+ );
8643
+ }
8644
+ return lines;
8645
+ }
8463
8646
  function statusLine(o, cols) {
8464
8647
  const phrasings = o.estimate === void 0 ? [] : typeof o.estimate === "string" ? [o.estimate] : [...o.estimate];
8648
+ const locked = lockedPhrasings(o.usable);
8465
8649
  for (const estimate of [...phrasings, void 0]) {
8466
- const line2 = statusRow(o, estimate, cols);
8467
- if (estimate === void 0 || visibleLength(line2) <= cols) return fitRow(line2, cols);
8650
+ for (const held of [...locked, void 0]) {
8651
+ const line2 = statusRow(o, estimate, held, cols);
8652
+ if (visibleLength(line2) <= cols) return line2;
8653
+ }
8468
8654
  }
8469
- return fitRow(statusRow(o, void 0, cols), cols);
8655
+ return fitRow(statusRow(o, void 0, void 0, cols), cols);
8470
8656
  }
8471
- function statusRow(o, estimate, cols) {
8657
+ function statusRow(o, estimate, locked, cols) {
8472
8658
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
8473
8659
  const clauses = [
8474
8660
  { text: o.model, rank: KEEP_ALWAYS },
@@ -8477,9 +8663,15 @@ function statusRow(o, estimate, cols) {
8477
8663
  {
8478
8664
  // "? cr" rather than "0 cr": unknown is not empty, and an empty-looking
8479
8665
  // wallet is the one misreading on this row that frightens somebody.
8480
- text: o.balance === void 0 ? "? cr" : `${o.balance.toLocaleString("en-US")} cr`,
8666
+ // With part of the balance held back, what can be spent takes its place:
8667
+ // "33,396 cr" over a wallet the gateway refused to spend from was how
8668
+ // this row came to be read as a fault (2026-10-12).
8669
+ text: o.balance === void 0 ? "? cr" : o.usable && o.usable.locked > 0 ? `${o.usable.credits.toLocaleString("en-US")} cr usable` : `${o.balance.toLocaleString("en-US")} cr`,
8481
8670
  rank: KEEP_ALWAYS
8482
8671
  },
8672
+ // Never dropped inside a row: `statusLine` tries its shorter phrasings,
8673
+ // then none, as it does for the preview.
8674
+ { text: locked, rank: 5 },
8483
8675
  // Above the context warning, below the balance. It is the second number
8484
8676
  // deciding whether the next turn costs anything, and unlike the spend it is
8485
8677
  // *news* — it only exists while a free model is selected and it counts down.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.31",
3
+ "version": "0.0.1-beta.32",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",