clixad 0.0.1-beta.11 → 0.0.1-beta.13

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 +558 -181
  2. package/package.json +6 -2
package/dist/clixad.mjs CHANGED
@@ -197,6 +197,7 @@ var init_client = __esm({
197
197
  signup_bonus: "signup bonus",
198
198
  ad_reward: "offer reward",
199
199
  ad_screenout_bonus: "screenout bonus (didn't qualify)",
200
+ ad_sponsor_impression: "sponsored line shown",
200
201
  ad_reversal: "offer reward reversed by the provider",
201
202
  purchase: "credits purchased",
202
203
  usage: "model usage",
@@ -369,6 +370,92 @@ var init_client = __esm({
369
370
  if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
370
371
  return res.json();
371
372
  }
373
+ /**
374
+ * Ask the gateway for a sponsored line, or `null` if it has none.
375
+ *
376
+ * **This never throws and never reports a failure to anybody.** Every caller is
377
+ * painting a row of dim chrome that already has a house line in it, so there is
378
+ * no state a user could act on and nothing a message would improve — a 404 from
379
+ * a gateway older than the route, a 401, a cold start, a dropped connection and
380
+ * an empty inventory are all the same event here, which is "keep showing ours".
381
+ * That is the opposite of every other method on this client, and it is why the
382
+ * catch is this broad rather than a list of statuses.
383
+ *
384
+ * The serve itself happens on the gateway, deliberately: an impression is worth
385
+ * credits, and this program is installed by strangers from npm. All the CLI
386
+ * gets to decide is when to ask.
387
+ */
388
+ /**
389
+ * The serve parameters for this session, or `null` for "no ad network".
390
+ *
391
+ * Fetched once per session rather than per model call: the publisher id and
392
+ * the subject are stable for the life of an account, and the SDK's slot holds
393
+ * its own dwell cache. This route used to be `GET /v1/sponsor/line`, and used
394
+ * to be asked before every single call.
395
+ *
396
+ * Never throws, for the reason `sponsorLine` never threw: the caller starts it
397
+ * beside a turn that is already running, and a status line must not be able to
398
+ * take a session with it. A 404 (an older gateway that still serves lines
399
+ * itself) and a 401 both land on `null`, which means the house line — the
400
+ * correct outcome in both cases.
401
+ */
402
+ async sponsorConfig(signal) {
403
+ try {
404
+ const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
405
+ headers: this.headers(),
406
+ ...signal ? { signal } : {}
407
+ });
408
+ if (!res.ok) return null;
409
+ const body = await res.json();
410
+ const publisherId = typeof body.publisher_id === "string" ? body.publisher_id.trim() : "";
411
+ if (!publisherId) return null;
412
+ return {
413
+ publisherId,
414
+ ...typeof body.category === "string" ? { category: body.category } : {},
415
+ ...typeof body.subject === "string" ? { subject: body.subject } : {},
416
+ ...typeof body.dwell_ms === "number" ? { dwellMs: body.dwell_ms } : {}
417
+ };
418
+ } catch {
419
+ return null;
420
+ }
421
+ }
422
+ /**
423
+ * Report one impression, and answer what it was credited.
424
+ *
425
+ * Fire-and-forget from the caller's point of view — the number comes back for
426
+ * the session tally and for tests, and nothing on screen depends on it. Like
427
+ * `sponsorConfig` it never throws: this is called from a `void`, and an
428
+ * unhandled rejection there is a Node process exiting over a line of chrome
429
+ * nobody asked for.
430
+ *
431
+ * **Call this after the model call the line was shown during has finished.**
432
+ * The gateway credits an impression only against a *settled* model call, and a
433
+ * report that arrives while the call is still in flight is refused every time
434
+ * — silently, since a refused grant is a 200 with `credited: 0`. That is the
435
+ * shape this shipped in once. `reportSponsor` in `app.tsx` is the caller, and
436
+ * it fires from `runTurn`'s `finally` for exactly this reason.
437
+ */
438
+ async reportImpression(report, signal) {
439
+ try {
440
+ const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/impression`, {
441
+ method: "POST",
442
+ headers: this.headers(),
443
+ ...signal ? { signal } : {},
444
+ body: JSON.stringify({
445
+ impression_id: report.impressionId,
446
+ ...report.adId ? { ad_id: report.adId } : {},
447
+ billable: report.billable,
448
+ from_cache: report.fromCache,
449
+ credit: report.credit
450
+ })
451
+ });
452
+ if (!res.ok) return 0;
453
+ const body = await res.json();
454
+ return typeof body.credited === "number" ? body.credited : 0;
455
+ } catch {
456
+ return 0;
457
+ }
458
+ }
372
459
  /**
373
460
  * Search the web, through the gateway.
374
461
  *
@@ -3626,6 +3713,8 @@ If it's installed under a different name/path, set CLIXAD_KIMI_BIN.`;
3626
3713
 
3627
3714
  // src/banner.ts
3628
3715
  import { homedir as homedir6 } from "node:os";
3716
+ import sliceAnsi from "slice-ansi";
3717
+ import stringWidth from "string-width";
3629
3718
  function duckLines() {
3630
3719
  const lines = [];
3631
3720
  for (let row = 0; row < DUCK.length; row += 2) {
@@ -3709,22 +3798,29 @@ function welcomeBanner(opts) {
3709
3798
  return out.join("\n");
3710
3799
  }
3711
3800
  function compactBanner(opts) {
3712
- const welcome = opts.name ? `Welcome back, ${opts.name}!` : "Welcome to Clixad!";
3801
+ const width = Math.max(1, opts.width ?? MIN_BANNER_WIDTH);
3802
+ const welcome = truncate(opts.name ? `Welcome back, ${opts.name}!` : "Welcome to Clixad!", Math.max(1, width - 2));
3713
3803
  const hint = bannerTip(opts.tipIndex);
3714
- return duckLines().map((d) => " " + d).join("\n") + `
3715
-
3716
- ${BOLD}${TEXT}${welcome}${R2}
3717
- ${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}${hint.desc}${R2} ${FAINT}\xB7${R2} ${opts.balance.toLocaleString("en-US")} credits
3804
+ const balance = `${opts.balance.toLocaleString("en-US")} credits`;
3805
+ const room = width - (4 + stringWidth(hint.key) + 1 + 4 + stringWidth(balance));
3806
+ const desc = room >= 3 ? truncate(hint.desc, room) : "";
3807
+ const tipRow = desc ? ` ${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}${desc}${R2} ${FAINT}\xB7${R2} ${balance}` : truncate(` ${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}\xB7${R2} ${balance}`, width);
3808
+ const duck = width >= DUCK_W + 2 ? duckLines().map((d) => " " + d).join("\n") + "\n\n" : "";
3809
+ return `${duck} ${BOLD}${TEXT}${welcome}${R2}
3810
+ ${tipRow}
3718
3811
  `;
3719
3812
  }
3720
- function clearScreen() {
3721
- const out = process.stdout;
3722
- if (!out.isTTY && !process.stdin.isTTY) return;
3813
+ function clearScreen(out = process.stdout) {
3814
+ if (!out.isTTY) return;
3723
3815
  out.write("\n".repeat(out.rows ?? 40));
3724
3816
  out.write("\x1B[2J\x1B[3J\x1B[H\x1B[0f");
3725
3817
  }
3726
3818
  function visibleLength(s) {
3727
- return s.replace(/\x1b\[[0-9;]*m/g, "").length;
3819
+ return stringWidth(s);
3820
+ }
3821
+ function sliceColumns(s, columns) {
3822
+ if (columns <= 0) return "";
3823
+ return stringWidth(s) <= columns ? s : sliceAnsi(s, 0, columns);
3728
3824
  }
3729
3825
  var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, BANNER_TIPS, DUCK_FRAMES, BRAILLE_FRAMES, SPINNER_TICKS_PER_FRAME, MIN_BANNER_WIDTH, RIGHT_MAX;
3730
3826
  var init_banner = __esm({
@@ -3767,7 +3863,7 @@ var init_banner = __esm({
3767
3863
  DUCK_W = 14;
3768
3864
  cell = (text, w) => ({ text, w });
3769
3865
  padTo = (c2, width) => (c2?.text ?? "") + " ".repeat(Math.max(0, width - (c2?.w ?? 0)));
3770
- truncate = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
3866
+ truncate = (s, max) => stringWidth(s) > max ? sliceColumns(s, Math.max(0, max - 1)) + "\u2026" : s;
3771
3867
  BANNER_TIPS = [
3772
3868
  { key: "/init", desc: "make a CLIXAD.md" },
3773
3869
  { key: "shift+tab", desc: "switch mode" },
@@ -3789,6 +3885,45 @@ var init_banner = __esm({
3789
3885
  }
3790
3886
  });
3791
3887
 
3888
+ // src/color.ts
3889
+ function colorEnabled({ isTTY, env }) {
3890
+ const forced = env.FORCE_COLOR?.trim().toLowerCase();
3891
+ if (forced === "0" || forced === "false") return false;
3892
+ if (forced !== void 0 && forced !== "") return true;
3893
+ if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
3894
+ if (!isTTY) return false;
3895
+ if (env.TERM === "dumb") return false;
3896
+ return true;
3897
+ }
3898
+ function palette(input) {
3899
+ const on = colorEnabled(input);
3900
+ const paint = (code) => on ? (s) => `\x1B[${code}m${s}\x1B[0m` : PLAIN;
3901
+ return {
3902
+ cyan: paint(CODES.cyan),
3903
+ green: paint(CODES.green),
3904
+ yellow: paint(CODES.yellow),
3905
+ red: paint(CODES.red),
3906
+ dim: paint(CODES.dim),
3907
+ bold: paint(CODES.bold),
3908
+ enabled: on
3909
+ };
3910
+ }
3911
+ var CODES, PLAIN;
3912
+ var init_color = __esm({
3913
+ "src/color.ts"() {
3914
+ "use strict";
3915
+ CODES = {
3916
+ cyan: "36",
3917
+ green: "32",
3918
+ yellow: "33",
3919
+ red: "31",
3920
+ dim: "2",
3921
+ bold: "1"
3922
+ };
3923
+ PLAIN = (s) => s;
3924
+ }
3925
+ });
3926
+
3792
3927
  // src/version.ts
3793
3928
  import { readFileSync as readFileSync6 } from "node:fs";
3794
3929
  function readVersion() {
@@ -4002,6 +4137,23 @@ function sponsorSource(provider = "house") {
4002
4137
  return houseSource();
4003
4138
  }
4004
4139
  }
4140
+ function adtentionSource(fetchLine) {
4141
+ return {
4142
+ provider: "adtention",
4143
+ async fetch(signal) {
4144
+ let line2;
4145
+ try {
4146
+ line2 = await fetchLine(signal);
4147
+ } catch {
4148
+ return null;
4149
+ }
4150
+ if (!line2) return null;
4151
+ const text = line2.text.trim();
4152
+ if (!text) return null;
4153
+ return { ...line2, text };
4154
+ }
4155
+ };
4156
+ }
4005
4157
  var init_provider = __esm({
4006
4158
  "src/sponsor/provider.ts"() {
4007
4159
  "use strict";
@@ -4022,19 +4174,98 @@ function sponsorEnabled({ isTTY, env, configEnabled }) {
4022
4174
  if (env.CI !== void 0 && env.CI !== "") return false;
4023
4175
  return true;
4024
4176
  }
4025
- function sponsorText(line2, cols) {
4026
- const indent = " ";
4027
- const prefix = `${indent}${SPONSOR_MARKER} `;
4028
- const room = cols - prefix.length;
4029
- if (room < 12) return null;
4030
- const body = line2.length <= room ? line2 : `${line2.slice(0, Math.max(0, room - 1)).trimEnd()}\u2026`;
4031
- return prefix + body;
4177
+ function adFitsWidth(cols) {
4178
+ return cols >= AD_MIN_COLS;
4179
+ }
4180
+ function sponsorRow(line2, cols, origin) {
4181
+ const marker = origin === "house" || cols >= AD_FULL_PREFIX_MIN_COLS ? HOUSE_MARKER_ROW : AD_MARKER_ROW;
4182
+ const label = origin === "network" ? LABEL_ROW : null;
4183
+ if (origin === "network" && !adFitsWidth(cols)) return null;
4184
+ const room = cols - visibleLength(marker) - (label ? visibleLength(label) : 0);
4185
+ if (room < MIN_BODY_COLS) return null;
4186
+ const body = visibleLength(line2) <= room ? line2 : `${sliceColumns(line2, room - 1).trimEnd()}\u2026`;
4187
+ return { marker, label, body, origin };
4188
+ }
4189
+ function adColorEnabled({ isTTY, env }) {
4190
+ if (!colorEnabled({ isTTY, env })) return false;
4191
+ if (env.CI !== void 0 && env.CI !== "") return false;
4192
+ return true;
4032
4193
  }
4033
- var SPONSOR_MARKER;
4194
+ 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;
4034
4195
  var init_render = __esm({
4035
4196
  "src/sponsor/render.ts"() {
4036
4197
  "use strict";
4037
- SPONSOR_MARKER = "\u2726 clixad \xB7";
4198
+ init_banner();
4199
+ init_color();
4200
+ SPONSOR_SIGIL = "\u2726";
4201
+ SPONSOR_MARKER = `${SPONSOR_SIGIL} clixad \xB7`;
4202
+ AD_LABEL = "Ad";
4203
+ INDENT = " ";
4204
+ HOUSE_MARKER_ROW = `${INDENT}${SPONSOR_MARKER} `;
4205
+ AD_MARKER_ROW = `${INDENT}${SPONSOR_SIGIL} `;
4206
+ LABEL_ROW = `${AD_LABEL} \xB7 `;
4207
+ MIN_BODY_COLS = 12;
4208
+ SHORT_AD_COLS = 29;
4209
+ AD_MIN_COLS = visibleLength(AD_MARKER_ROW) + visibleLength(LABEL_ROW) + SHORT_AD_COLS;
4210
+ AD_FULL_PREFIX_MIN_COLS = visibleLength(HOUSE_MARKER_ROW) + visibleLength(LABEL_ROW) + SHORT_AD_COLS;
4211
+ }
4212
+ });
4213
+
4214
+ // src/sponsor/network.ts
4215
+ import { isCategory, SponsorSlot } from "@adtention/sdk";
4216
+ function sponsorLineText(ad) {
4217
+ const text = typeof ad.text === "string" ? ad.text.trim() : "";
4218
+ if (!text) return void 0;
4219
+ return text.length > MAX_TEXT_CHARS3 ? text.slice(0, MAX_TEXT_CHARS3).trimEnd() : text;
4220
+ }
4221
+ function createSponsorSlot(config) {
4222
+ const slot = new SponsorSlot({
4223
+ publisherId: config.publisherId,
4224
+ serveOnly: true,
4225
+ // `isCategory` is the SDK's own guard, and it is checked here even though
4226
+ // the gateway already validated `ADTENTION_CATEGORY` at boot — because the
4227
+ // interesting direction is the other one: an older CLI against a newer
4228
+ // gateway that has learned a seventh category. Passing an unknown value
4229
+ // through would be rejected per serve and look exactly like empty
4230
+ // inventory; dropping it falls back to `general`, which the SDK documents
4231
+ // as always filling from broad inventory. A worse-targeted line beats none.
4232
+ ...isCategory(config.category) ? { category: config.category } : {},
4233
+ ...config.dwellMs && config.dwellMs > 0 ? { dwellMs: config.dwellMs } : {},
4234
+ timeoutMs: 2500,
4235
+ // Deliberately silent. A failed serve is not something the person at the
4236
+ // keyboard can act on, and this program's output is a coding session — an ad
4237
+ // network's transport error printed into it would be the single most
4238
+ // annoying line in the product.
4239
+ onError: () => {
4240
+ }
4241
+ });
4242
+ return {
4243
+ async serve(cols, signal) {
4244
+ if (!adFitsWidth(cols)) return null;
4245
+ const ad = await slot.next({
4246
+ ...config.subject ? { subject: config.subject } : {},
4247
+ ...isCategory(config.category) ? { category: config.category } : {}
4248
+ });
4249
+ if (signal?.aborted || !ad) return null;
4250
+ const text = sponsorLineText(ad);
4251
+ if (!text) return null;
4252
+ return {
4253
+ id: ad.adId,
4254
+ text,
4255
+ impressionId: ad.impressionId,
4256
+ billable: ad.billable,
4257
+ fromCache: ad.fromCache,
4258
+ credit: ad.credit
4259
+ };
4260
+ }
4261
+ };
4262
+ }
4263
+ var MAX_TEXT_CHARS3;
4264
+ var init_network = __esm({
4265
+ "src/sponsor/network.ts"() {
4266
+ "use strict";
4267
+ init_render();
4268
+ MAX_TEXT_CHARS3 = 200;
4038
4269
  }
4039
4270
  });
4040
4271
 
@@ -4398,48 +4629,42 @@ var init_markdown = __esm({
4398
4629
  // src/tui/views.tsx
4399
4630
  import "react";
4400
4631
  import { Box, Text } from "ink";
4632
+ import wrapAnsi from "wrap-ansi";
4401
4633
  import { jsx, jsxs } from "react/jsx-runtime";
4634
+ function wrapRows(text, cols) {
4635
+ return wrapAnsi(text, Math.max(1, cols), WRAP_LIKE_INK).split("\n");
4636
+ }
4402
4637
  function lineCount(text, cols) {
4403
- return text.split("\n").reduce((n, line2) => n + Math.max(1, Math.ceil(visibleLength(line2) / Math.max(1, cols))), 0);
4638
+ return wrapRows(text, cols).length;
4404
4639
  }
4405
4640
  function tailRows(text, cols, max) {
4406
4641
  if (max <= 0) return "";
4407
- const width = Math.max(1, cols);
4408
- const lines = text.split("\n");
4409
- const kept = [];
4410
- let used = 0;
4411
- for (let i = lines.length - 1; i >= 0; i--) {
4412
- const line2 = lines[i];
4413
- const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
4414
- if (used + h <= max) {
4415
- kept.unshift(line2);
4416
- used += h;
4417
- continue;
4418
- }
4419
- const room = max - used;
4420
- if (room > 0) kept.unshift(line2.slice(-(room * width)));
4421
- break;
4422
- }
4423
- return kept.join("\n");
4642
+ const rows = wrapRows(text, cols);
4643
+ if (rows.length <= max) return text;
4644
+ return rows.slice(-max).join("\n");
4645
+ }
4646
+ function fitRow(text, cols) {
4647
+ const width = Math.max(0, cols);
4648
+ if (width === 0) return "";
4649
+ if (visibleLength(text) <= width) return text;
4650
+ if (width === 1) return "\u2026";
4651
+ return `${sliceColumns(text, width - 1)}\u2026`;
4652
+ }
4653
+ function pickHint(variants, cols) {
4654
+ for (const variant of variants) if (visibleLength(variant) <= cols) return variant;
4655
+ return fitRow(variants[variants.length - 1] ?? "", cols);
4656
+ }
4657
+ function padColumns(text, width) {
4658
+ return text + " ".repeat(Math.max(0, width - visibleLength(text)));
4659
+ }
4660
+ function dialogInner(cols) {
4661
+ return Math.max(1, cols - DIALOG_CHROME_COLS);
4424
4662
  }
4425
4663
  function headRows(text, cols, max) {
4426
4664
  if (max <= 0) return "";
4427
- const width = Math.max(1, cols);
4428
- const lines = text.split("\n");
4429
- const kept = [];
4430
- let used = 0;
4431
- for (const line2 of lines) {
4432
- const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
4433
- if (used + h <= max) {
4434
- kept.push(line2);
4435
- used += h;
4436
- continue;
4437
- }
4438
- const room = max - used;
4439
- if (room > 0) kept.push(line2.slice(0, room * width));
4440
- break;
4441
- }
4442
- return kept.join("\n");
4665
+ const rows = wrapRows(text, cols);
4666
+ if (rows.length <= max) return text;
4667
+ return rows.slice(0, max).join("\n");
4443
4668
  }
4444
4669
  function errorNotice(message, rows = ERROR_NOTICE_ROWS) {
4445
4670
  const clean = message.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)?/g, " ").replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g, " ");
@@ -4494,7 +4719,7 @@ function EntryView({ entry }) {
4494
4719
  ) });
4495
4720
  }
4496
4721
  }
4497
- var MANGO3, MANGO_BRIGHT, SLATE, MODE_STYLE, ERROR_NOTICE_ROWS, ERROR_NOTICE_COLS;
4722
+ var MANGO3, MANGO_BRIGHT, SLATE, AD_TEXT, AD_MARK, MODE_STYLE, WRAP_LIKE_INK, DIALOG_CHROME_COLS, ERROR_NOTICE_ROWS, ERROR_NOTICE_COLS;
4498
4723
  var init_views = __esm({
4499
4724
  "src/tui/views.tsx"() {
4500
4725
  "use strict";
@@ -4503,37 +4728,52 @@ var init_views = __esm({
4503
4728
  MANGO3 = "#f5b841";
4504
4729
  MANGO_BRIGHT = "#ffcf6b";
4505
4730
  SLATE = "#8b98ae";
4731
+ AD_TEXT = "#8a80b4";
4732
+ AD_MARK = "#6b6390";
4506
4733
  MODE_STYLE = {
4507
4734
  normal: { color: SLATE, glyph: "\u23F5" },
4508
4735
  acceptEdits: { color: "#7ee787", glyph: "\u23F5\u23F5" },
4509
4736
  plan: { color: "#79c0ff", glyph: "\u23F8" }
4510
4737
  };
4738
+ WRAP_LIKE_INK = { trim: false, hard: true };
4739
+ DIALOG_CHROME_COLS = 4;
4511
4740
  ERROR_NOTICE_ROWS = 12;
4512
4741
  ERROR_NOTICE_COLS = 200;
4513
4742
  }
4514
4743
  });
4515
4744
 
4516
4745
  // src/tui/app.tsx
4517
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4746
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4518
4747
  import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
4519
4748
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4520
4749
  function useTerminalSize() {
4521
- const [size, setSize] = useState({
4522
- rows: process.stdout.rows ?? 24,
4523
- cols: process.stdout.columns ?? 80
4750
+ const { stdout } = useStdout();
4751
+ const snapshot2 = useRef({
4752
+ rows: stdout.rows ?? FALLBACK_ROWS,
4753
+ cols: stdout.columns ?? FALLBACK_COLS
4524
4754
  });
4525
- useEffect(() => {
4526
- const onResize = () => setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });
4527
- process.stdout.on("resize", onResize);
4528
- return () => {
4529
- process.stdout.off("resize", onResize);
4530
- };
4531
- }, []);
4532
- return size;
4755
+ const subscribe = useCallback(
4756
+ (onChange) => {
4757
+ stdout.prependListener("resize", onChange);
4758
+ return () => {
4759
+ stdout.off("resize", onChange);
4760
+ };
4761
+ },
4762
+ [stdout]
4763
+ );
4764
+ const getSnapshot = useCallback(() => {
4765
+ const rows = stdout.rows ?? FALLBACK_ROWS;
4766
+ const cols = stdout.columns ?? FALLBACK_COLS;
4767
+ if (rows !== snapshot2.current.rows || cols !== snapshot2.current.cols) {
4768
+ snapshot2.current = { rows, cols };
4769
+ }
4770
+ return snapshot2.current;
4771
+ }, [stdout]);
4772
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
4533
4773
  }
4534
- function App({ client, config, wallet, session, initialTask }) {
4774
+ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
4535
4775
  const { exit } = useApp();
4536
- const { write: writeToStdout } = useStdout();
4776
+ const { stdout, write: writeToStdout } = useStdout();
4537
4777
  const idRef = useRef(1);
4538
4778
  const { rows, cols } = useTerminalSize();
4539
4779
  const root = process.cwd();
@@ -4541,8 +4781,12 @@ function App({ client, config, wallet, session, initialTask }) {
4541
4781
  {
4542
4782
  id: 0,
4543
4783
  kind: "banner",
4544
- text: ((process.stdout.columns ?? 80) >= MIN_BANNER_WIDTH ? welcomeBanner : compactBanner)({
4545
- width: process.stdout.columns ?? 80,
4784
+ // The width ink is actually laying out against. Read once, because a
4785
+ // committed <Static> line is written to the scrollback and belongs to the
4786
+ // terminal from then on — but "once" now means the width at mount rather
4787
+ // than whatever `process.stdout` happened to say.
4788
+ text: (cols >= MIN_BANNER_WIDTH ? welcomeBanner : compactBanner)({
4789
+ width: cols,
4546
4790
  name: config.login ?? config.email?.split("@")[0],
4547
4791
  model: config.model,
4548
4792
  balance: wallet?.balance ?? 0,
@@ -4605,6 +4849,38 @@ function App({ client, config, wallet, session, initialTask }) {
4605
4849
  const sponsorRef = useRef(sponsorSource());
4606
4850
  const sponsorIdxRef = useRef(0);
4607
4851
  const sponsorPrevRef = useRef(void 0);
4852
+ const sponsorCallRef = useRef(0);
4853
+ const pendingImpressionRef = useRef(null);
4854
+ const sponsorSlotRef = useRef(null);
4855
+ const colsRef = useRef(cols);
4856
+ colsRef.current = cols;
4857
+ const sponsorConfigRef = useRef(null);
4858
+ const remoteSponsorRef = useRef(
4859
+ adtentionSource(async (signal) => {
4860
+ let served;
4861
+ if (sponsorServe) {
4862
+ served = await sponsorServe(colsRef.current, signal);
4863
+ } else {
4864
+ sponsorConfigRef.current ??= client.sponsorConfig(signal);
4865
+ const conf = await sponsorConfigRef.current;
4866
+ if (!conf) return null;
4867
+ sponsorSlotRef.current ??= createSponsorSlot(conf);
4868
+ served = await sponsorSlotRef.current.serve(colsRef.current, signal);
4869
+ }
4870
+ if (!served) return null;
4871
+ return {
4872
+ id: served.id,
4873
+ text: served.text,
4874
+ impression: {
4875
+ impressionId: served.impressionId,
4876
+ adId: served.id,
4877
+ billable: served.billable,
4878
+ fromCache: served.fromCache,
4879
+ credit: served.credit
4880
+ }
4881
+ };
4882
+ })
4883
+ );
4608
4884
  const tallyRef = useRef(createTally());
4609
4885
  const heightCacheRef = useRef(/* @__PURE__ */ new Map());
4610
4886
  const push = useCallback((e) => {
@@ -4692,8 +4968,9 @@ function App({ client, config, wallet, session, initialTask }) {
4692
4968
  );
4693
4969
  const nextSponsor = useCallback(
4694
4970
  (modelId) => {
4971
+ const call = ++sponsorCallRef.current;
4695
4972
  if (!sponsorEnabled({
4696
- isTTY: process.stdout.isTTY,
4973
+ isTTY: stdout.isTTY,
4697
4974
  env: process.env,
4698
4975
  configEnabled: config.sponsor
4699
4976
  })) {
@@ -4706,16 +4983,31 @@ function App({ client, config, wallet, session, initialTask }) {
4706
4983
  adsPerTask: est === void 0 ? void 0 : adsPerTaskLabel(est)
4707
4984
  };
4708
4985
  const picked = sponsorRef.current.pick(sponsorIdxRef.current++, sponsorPrevRef.current, facts);
4709
- if (!picked) {
4986
+ if (picked) {
4987
+ sponsorPrevRef.current = picked.id;
4988
+ recordImpression(tallyRef.current, picked.id);
4989
+ setSponsor({ text: picked.text, origin: "house" });
4990
+ } else {
4710
4991
  setSponsor(null);
4711
- return;
4712
4992
  }
4713
- sponsorPrevRef.current = picked.id;
4714
- recordImpression(tallyRef.current, picked.id);
4715
- setSponsor(picked.text);
4993
+ if (!adFitsWidth(colsRef.current)) return;
4994
+ void remoteSponsorRef.current.fetch().then((line2) => {
4995
+ if (!line2 || call !== sponsorCallRef.current) return;
4996
+ if (!sponsorRow(line2.text, colsRef.current, "network")) return;
4997
+ sponsorPrevRef.current = line2.id;
4998
+ recordImpression(tallyRef.current, line2.id);
4999
+ setSponsor({ text: line2.text, origin: "network" });
5000
+ pendingImpressionRef.current = { call, impression: line2.impression };
5001
+ });
4716
5002
  },
4717
- [config.sponsor, wallet]
5003
+ [config.sponsor, stdout, wallet]
4718
5004
  );
5005
+ const reportSponsor = useCallback(() => {
5006
+ const pending = pendingImpressionRef.current;
5007
+ pendingImpressionRef.current = null;
5008
+ if (!pending || pending.call !== sponsorCallRef.current) return;
5009
+ void client.reportImpression(pending.impression);
5010
+ }, [client]);
4719
5011
  const askUser = useCallback(
4720
5012
  (req) => new Promise((resolve2) => setAsk({ req, resolve: resolve2 })),
4721
5013
  []
@@ -4800,7 +5092,7 @@ function App({ client, config, wallet, session, initialTask }) {
4800
5092
  return;
4801
5093
  }
4802
5094
  const lines = event.result.split("\n").filter((l) => l.trim() !== "");
4803
- const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
5095
+ const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => fitRow(l, Math.max(20, cols - 4)));
4804
5096
  const more = Math.max(0, lines.length - shown.length);
4805
5097
  const id = push({
4806
5098
  kind: "tool",
@@ -4939,6 +5231,8 @@ ${NO_SEARCH_INSTRUCTION}`),
4939
5231
  dropDelta();
4940
5232
  setLive(null);
4941
5233
  setBusy(false);
5234
+ reportSponsor();
5235
+ sponsorCallRef.current++;
4942
5236
  setSponsor(null);
4943
5237
  }
4944
5238
  },
@@ -4950,6 +5244,7 @@ ${NO_SEARCH_INSTRUCTION}`),
4950
5244
  dropDelta,
4951
5245
  handleEvent,
4952
5246
  model,
5247
+ reportSponsor,
4953
5248
  nextSponsor,
4954
5249
  permit,
4955
5250
  push,
@@ -5268,7 +5563,14 @@ ${NO_SEARCH_INSTRUCTION}`),
5268
5563
  push({
5269
5564
  kind: "notice",
5270
5565
  text: list.map(
5271
- (s) => ` ${s.id.padEnd(8)} ${s.status.padEnd(8)} ${Math.round(s.runningMs / 1e3)}s ${s.command.slice(0, Math.max(20, cols - 32))}`
5566
+ (s) => (
5567
+ // A command is arbitrary text, so it is cut in columns like
5568
+ // everything else that is measured against the width.
5569
+ fitRow(
5570
+ ` ${s.id.padEnd(8)} ${s.status.padEnd(8)} ${Math.round(s.runningMs / 1e3)}s ${s.command}`,
5571
+ Math.max(28, cols)
5572
+ )
5573
+ )
5272
5574
  ).join("\n").concat("\n /bg kill <id> \xB7 /bg kill all")
5273
5575
  });
5274
5576
  return;
@@ -5322,7 +5624,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5322
5624
  { command }
5323
5625
  );
5324
5626
  const lines = output.split("\n").filter((l) => l.trim() !== "");
5325
- const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
5627
+ const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => fitRow(l, Math.max(20, cols - 4)));
5326
5628
  const more = Math.max(0, lines.length - shown.length);
5327
5629
  const id = push({
5328
5630
  kind: "tool",
@@ -5562,36 +5864,82 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
5562
5864
  });
5563
5865
  const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
5564
5866
  const spinner = spinnerRef.current[Math.floor(tick / SPINNER_TICKS_PER_FRAME) % spinnerRef.current.length];
5565
- const sponsorLine = busy && sponsor ? sponsorText(sponsor, cols) : null;
5566
- const viewport = Math.max(6, rows - 1);
5867
+ const sponsorLine = busy && sponsor ? sponsorRow(sponsor.text, cols, sponsor.origin) : null;
5868
+ const adColor = adColorEnabled({ isTTY: stdout.isTTY, env: process.env });
5869
+ const viewport = Math.max(1, rows - 1);
5567
5870
  const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
5568
5871
  const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
5569
- const inputBoxHeight = 2 + inputRows;
5570
- const queueRows = [
5571
- ...queueView.slice(0, MAX_QUEUE_ROWS).map((line2) => line2.replace(/\s+/g, " ").slice(0, Math.max(10, cols - 6))),
5872
+ const queuedRows = [
5873
+ ...queueView.slice(0, MAX_QUEUE_ROWS).map((line2) => line2.replace(/\s+/g, " ")),
5572
5874
  ...queueView.length > MAX_QUEUE_ROWS ? [`+${queueView.length - MAX_QUEUE_ROWS} more queued`] : []
5573
- ];
5574
- const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0) + queueRows.length;
5575
- let budget = Math.max(0, viewport - chromeHeight - menu.length);
5576
- const PICKER_CHROME = 8;
5577
- const pickerRows = picker ? Math.max(1, Math.min(MAX_PICKER_ROWS, picker.items.length, budget - PICKER_CHROME)) : 0;
5875
+ ].map((line2) => fitRow(` ${QUEUE_MARKER} ${line2}`, cols));
5876
+ const inner = dialogInner(cols);
5877
+ const modalFloor = picker ? PICKER_CHROME_COMPACT + 1 : ask2 ? ASK_CHROME + 1 : plan ? PLAN_CHROME + 1 : 0;
5878
+ let slack = Math.max(0, viewport - inputRows - modalFloor - menu.length);
5879
+ const grant = (want) => {
5880
+ if (want <= 0 || want > slack) return 0;
5881
+ slack -= want;
5882
+ return want;
5883
+ };
5884
+ const inputBorder = grant(2) > 0;
5885
+ const busyHeight = grant(busy ? 2 : 0);
5886
+ const showStatus = grant(2) > 0;
5887
+ const queueRows = grant(queuedRows.length) > 0 ? queuedRows : [];
5888
+ const showSponsor = grant(sponsorLine ? 1 : 0) > 0;
5889
+ const inputBoxHeight = inputRows + (inputBorder ? 2 : 0);
5890
+ const chromeHeight = inputBoxHeight + (showStatus ? 2 : 0) + (showSponsor ? 1 : 0) + queueRows.length;
5891
+ let budget = Math.max(0, viewport - chromeHeight - menu.length - busyHeight);
5892
+ const pickerCompact = Boolean(picker) && budget < PICKER_CHROME + 1;
5893
+ const pickerChrome = pickerCompact ? PICKER_CHROME_COMPACT : PICKER_CHROME;
5894
+ const pickerRows = picker ? Math.max(1, Math.min(MAX_PICKER_ROWS, picker.items.length, budget - pickerChrome)) : 0;
5578
5895
  const pickerFrom = picker ? windowStart(pickerSel, picker.items.length, pickerRows) : 0;
5579
5896
  const pickerItems = picker ? picker.items.slice(pickerFrom, pickerFrom + pickerRows) : [];
5580
- const pickerHeight = picker ? pickerItems.length + PICKER_CHROME : 0;
5897
+ const pickerHeight = picker ? pickerItems.length + pickerChrome : 0;
5581
5898
  pickerWindowRef.current = { from: pickerFrom, count: pickerItems.length };
5582
- const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
5899
+ const pickerLabelW = picker ? Math.min(
5900
+ Math.max(1, Math.floor(inner * 0.6)),
5901
+ picker.items.reduce((w, i) => Math.max(w, visibleLength(i.label)), 0)
5902
+ ) : 0;
5903
+ const pickerViewRows = pickerItems.map((item, i) => {
5904
+ const index = pickerFrom + i;
5905
+ const marker = item.current ? PICKER_CURRENT : "";
5906
+ const markerW = Math.min(visibleLength(marker), Math.max(0, inner - MIN_PICKER_LABEL_COLS));
5907
+ const head = fitRow(
5908
+ `${index === pickerSel ? "\u276F " : " "}${i + 1}. ${padColumns(item.label, pickerLabelW + 2)}`,
5909
+ Math.max(1, inner - markerW)
5910
+ );
5911
+ const room = inner - visibleLength(head) - markerW;
5912
+ return {
5913
+ key: item.value,
5914
+ selected: index === pickerSel,
5915
+ head,
5916
+ hint: room > 1 ? fitRow(item.hint, room) : "",
5917
+ marker: markerW >= visibleLength(marker) ? marker : ""
5918
+ };
5919
+ });
5920
+ const pickerTitle = picker ? fitRow(picker.title, inner) : "";
5921
+ const pickerSubtitle = picker ? fitRow(picker.subtitle, inner) : "";
5922
+ const pickerHint = picker ? pickHint(
5923
+ picker.items.length > pickerItems.length ? [
5924
+ `\u2191\u2193 choose (${pickerFrom + 1}-${pickerFrom + pickerItems.length} of ${picker.items.length}) \xB7 1-${pickerItems.length} jump \xB7 \u23CE confirm \xB7 esc cancel`,
5925
+ `\u2191\u2193 ${pickerFrom + 1}-${pickerFrom + pickerItems.length}/${picker.items.length} \xB7 \u23CE confirm \xB7 esc cancel`,
5926
+ "\u2191\u2193 \xB7 \u23CE \xB7 esc"
5927
+ ] : [
5928
+ `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel`,
5929
+ `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump \xB7 \u23CE confirm \xB7 esc cancel`,
5930
+ "\u2191\u2193 \xB7 \u23CE \xB7 esc"
5931
+ ],
5932
+ inner
5933
+ ) : "";
5583
5934
  budget -= pickerHeight;
5584
- const ASK_CHROME = 4;
5585
- const askSummaryRows = ask2 ? lineCount(ask2.req.summary, cols) : 0;
5586
- const askPreview = ask2?.req.preview ? headRows(ask2.req.preview, cols, Math.max(1, budget - ASK_CHROME - askSummaryRows)) : "";
5587
- const askHeight = ask2 ? askSummaryRows + lineCount(askPreview, cols) * (askPreview ? 1 : 0) + ASK_CHROME : 0;
5935
+ const askSummary = ask2 ? headRows(ask2.req.summary, inner, Math.max(1, budget - ASK_CHROME)) : "";
5936
+ const askSummaryRows = ask2 ? lineCount(askSummary, inner) : 0;
5937
+ const askPreview = ask2?.req.preview ? headRows(ask2.req.preview, inner, Math.max(0, budget - ASK_CHROME - askSummaryRows)) : "";
5938
+ const askHeight = ask2 ? askSummaryRows + (askPreview ? lineCount(askPreview, inner) : 0) + ASK_CHROME : 0;
5588
5939
  budget -= askHeight;
5589
- const PLAN_CHROME = 6;
5590
- const planBlock = plan ? headRows(plan.plan, cols, Math.max(1, budget - PLAN_CHROME)) : "";
5591
- const planHeight = plan ? lineCount(planBlock, cols) + PLAN_CHROME : 0;
5940
+ const planBlock = plan ? headRows(renderMarkdown(plan.plan), inner, Math.max(1, budget - PLAN_CHROME)) : "";
5941
+ const planHeight = plan ? lineCount(planBlock, inner) + PLAN_CHROME : 0;
5592
5942
  budget -= planHeight;
5593
- const busyHeight = busy ? 2 : 0;
5594
- budget -= busyHeight;
5595
5943
  const liveRaw = [
5596
5944
  // Trailing blank lines are rows the clamp would spend on nothing, and a
5597
5945
  // streamed answer ends on one more often than not.
@@ -5615,7 +5963,10 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
5615
5963
  }, [entries, cols]);
5616
5964
  const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + busyHeight + liveHeight;
5617
5965
  const spacer = Math.max(0, viewport - printed - used);
5618
- const labelW = menu.reduce((w, c2) => Math.max(w, c2.label.length), 0);
5966
+ const labelW = Math.min(
5967
+ Math.max(1, Math.floor(cols * 0.5)),
5968
+ menu.reduce((w, c2) => Math.max(w, visibleLength(c2.label)), 0)
5969
+ );
5619
5970
  const contextPct = useMemo(() => {
5620
5971
  const window = contextWindow(model);
5621
5972
  if (!window) return 0;
@@ -5625,81 +5976,86 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
5625
5976
  ]);
5626
5977
  return used2 / window;
5627
5978
  }, [contextWindow, messages, model]);
5979
+ const modeText = ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}`;
5980
+ const statusText = fitRow(
5981
+ ` ${statusLine(
5982
+ { model, balance, spent, contextPct, minutes: (Date.now() - runStartedAtRef.current) / 6e4 },
5983
+ Math.max(1, cols - visibleLength(modeText) - 2)
5984
+ )}`,
5985
+ Math.max(0, cols - visibleLength(modeText))
5986
+ );
5987
+ const hintText = quitHint ? fitRow(" press ctrl+c again to quit", cols) : ` ${pickHint(
5988
+ ["\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"],
5989
+ Math.max(1, cols - 2)
5990
+ )}`;
5991
+ const menuRows = menu.map((item, i) => {
5992
+ const head = fitRow(`${i === sel ? " \u276F " : " "}${padColumns(item.label, labelW + 2)}`, cols);
5993
+ const room = cols - visibleLength(head);
5994
+ return { key: item.value, selected: i === sel, head, hint: room > 1 ? fitRow(item.hint, room) : "" };
5995
+ });
5996
+ const busyText = fitRow(`${busyLabel} ${elapsed}s \xB7 esc to stop`, Math.max(1, cols - visibleLength(`${spinner} `)));
5628
5997
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
5629
5998
  /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }, staticEpoch),
5630
5999
  spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
5631
6000
  liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
5632
- busy ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
6001
+ busyHeight > 0 ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
5633
6002
  /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
5634
6003
  spinner,
5635
6004
  " "
5636
6005
  ] }),
5637
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
5638
- busyLabel,
5639
- " ",
5640
- elapsed,
5641
- "s \xB7 esc to stop"
5642
- ] })
6006
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: busyText })
5643
6007
  ] }) : null,
5644
6008
  ask2 ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
5645
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: ask2.req.summary }),
6009
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: askSummary }),
5646
6010
  askPreview ? /* @__PURE__ */ jsx2(Text2, { children: askPreview }) : null,
5647
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc stops the turn" })
6011
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: pickHint(ASK_HINT, inner) })
5648
6012
  ] }) : null,
5649
6013
  plan ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MODE_STYLE.plan.color, paddingX: 1, children: [
5650
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: MODE_STYLE.plan.color, children: `${MODE_STYLE.plan.glyph} Ready to act on this plan?` }),
6014
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: MODE_STYLE.plan.color, wrap: "truncate-end", children: fitRow(`${MODE_STYLE.plan.glyph} Ready to act on this plan?`, inner) }),
5651
6015
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
5652
- /* @__PURE__ */ jsx2(Text2, { children: renderMarkdown(planBlock) }),
5653
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] yes, ask before edits \xB7 [a] yes, auto-accept edits \xB7 [n] keep planning \xB7 esc stops the turn" })
6016
+ /* @__PURE__ */ jsx2(Text2, { children: planBlock }),
6017
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: pickHint(PLAN_HINT, inner) })
5654
6018
  ] }) : null,
5655
6019
  picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
5656
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: picker.title }),
5657
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.subtitle }),
5658
- /* @__PURE__ */ jsx2(Box2, { height: 1 }),
5659
- pickerItems.map((item, i) => {
5660
- const index = pickerFrom + i;
5661
- return /* @__PURE__ */ jsxs2(Box2, { children: [
5662
- /* @__PURE__ */ jsxs2(Text2, { color: index === pickerSel ? MANGO_BRIGHT : void 0, bold: index === pickerSel, children: [
5663
- index === pickerSel ? "\u276F " : " ",
5664
- `${i + 1}. `,
5665
- item.label.padEnd(pickerLabelW + 2)
5666
- ] }),
5667
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
5668
- item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
5669
- ] }, item.value);
5670
- }),
5671
- /* @__PURE__ */ jsx2(Box2, { height: 1 }),
5672
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.items.length > pickerItems.length ? `\u2191\u2193 choose (${pickerFrom + 1}-${pickerFrom + pickerItems.length} of ${picker.items.length}) \xB7 1-${pickerItems.length} jump \xB7 \u23CE confirm \xB7 esc cancel` : `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel` })
6020
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: pickerTitle }),
6021
+ pickerCompact ? null : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pickerSubtitle }),
6022
+ pickerCompact ? null : /* @__PURE__ */ jsx2(Box2, { height: 1 }),
6023
+ pickerViewRows.map((row) => /* @__PURE__ */ jsxs2(Box2, { children: [
6024
+ /* @__PURE__ */ jsx2(Text2, { color: row.selected ? MANGO_BRIGHT : void 0, bold: row.selected, wrap: "truncate-end", children: row.head }),
6025
+ row.hint ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: row.hint }) : null,
6026
+ row.marker ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: row.marker }) : null
6027
+ ] }, row.key)),
6028
+ pickerCompact ? null : /* @__PURE__ */ jsx2(Box2, { height: 1 }),
6029
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pickerHint })
5673
6030
  ] }) : null,
5674
6031
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
5675
- queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u23F3 ${line2}` }, i)),
5676
- sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
5677
- /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO3 : MANGO_BRIGHT, paddingX: 1, children: [
6032
+ queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: line2 }, i)),
6033
+ showSponsor && sponsorLine ? /* @__PURE__ */ jsxs2(Box2, { children: [
6034
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: sponsorLine.marker }),
6035
+ sponsorLine.label ? /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_MARK } : { dimColor: true }, wrap: "truncate-end", children: sponsorLine.label }) : null,
6036
+ /* @__PURE__ */ jsx2(
6037
+ Text2,
6038
+ {
6039
+ ...adColor && sponsorLine.origin === "network" ? { color: AD_TEXT } : { dimColor: true },
6040
+ wrap: "truncate-end",
6041
+ children: sponsorLine.body
6042
+ }
6043
+ )
6044
+ ] }) : null,
6045
+ inputBorder ? /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO3 : MANGO_BRIGHT, paddingX: 1, children: [
5678
6046
  /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
5679
6047
  /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
5680
- ] }),
5681
- menu.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
5682
- /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? MANGO_BRIGHT : MANGO3, bold: i === sel, children: [
5683
- i === sel ? " \u276F " : " ",
5684
- item.label.padEnd(labelW + 2)
5685
- ] }),
5686
- /* @__PURE__ */ jsx2(Text2, { dimColor: i !== sel, children: item.hint })
5687
- ] }, item.value)),
5688
- quitHint || menu.length > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
5689
- " ",
5690
- quitHint ? "press ctrl+c again to quit" : "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close"
5691
6048
  ] }) : /* @__PURE__ */ jsxs2(Box2, { children: [
5692
- /* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, children: ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}` }),
5693
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
5694
- " ",
5695
- statusLine({
5696
- model,
5697
- balance,
5698
- spent,
5699
- contextPct,
5700
- minutes: (Date.now() - runStartedAtRef.current) / 6e4
5701
- })
5702
- ] })
6049
+ /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
6050
+ /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
6051
+ ] }),
6052
+ menuRows.map((row) => /* @__PURE__ */ jsxs2(Box2, { children: [
6053
+ /* @__PURE__ */ jsx2(Text2, { color: row.selected ? MANGO_BRIGHT : MANGO3, bold: row.selected, wrap: "truncate-end", children: row.head }),
6054
+ row.hint ? /* @__PURE__ */ jsx2(Text2, { dimColor: !row.selected, wrap: "truncate-end", children: row.hint }) : null
6055
+ ] }, row.key)),
6056
+ !showStatus ? null : quitHint || menu.length > 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: hintText }) : /* @__PURE__ */ jsxs2(Box2, { children: [
6057
+ /* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, wrap: "truncate-end", children: modeText }),
6058
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: statusText })
5703
6059
  ] })
5704
6060
  ] })
5705
6061
  ] });
@@ -5731,17 +6087,23 @@ function renderInput(state, from, rows) {
5731
6087
  ] }, row);
5732
6088
  });
5733
6089
  }
5734
- function statusLine(o) {
6090
+ function statusLine(o, cols) {
5735
6091
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
5736
- const parts = [
5737
- o.model,
5738
- `${o.balance.toLocaleString("en-US")} cr`,
5739
- o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0,
5740
- burn,
5741
- o.contextPct >= CONTEXT_NOTICE_AT ? `context ${Math.round(o.contextPct * 100)}%` : void 0,
5742
- "/help"
5743
- ].filter(Boolean);
5744
- return parts.join(" \xB7 ");
6092
+ const clauses = [
6093
+ { text: o.model, rank: KEEP_ALWAYS },
6094
+ { text: `${o.balance.toLocaleString("en-US")} cr`, rank: KEEP_ALWAYS },
6095
+ { text: o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0, rank: 2 },
6096
+ { text: burn, rank: 1 },
6097
+ {
6098
+ text: o.contextPct >= CONTEXT_NOTICE_AT ? `context ${Math.round(o.contextPct * 100)}%` : void 0,
6099
+ rank: 3
6100
+ },
6101
+ { text: "/help", rank: 0 }
6102
+ ].filter((c2) => Boolean(c2.text));
6103
+ const join7 = (minRank) => clauses.filter((c2) => c2.rank >= minRank).map((c2) => c2.text).join(" \xB7 ");
6104
+ let line2 = join7(0);
6105
+ for (let minRank = 1; minRank <= 4 && visibleLength(line2) > cols; minRank++) line2 = join7(minRank);
6106
+ return fitRow(line2, cols);
5745
6107
  }
5746
6108
  function windowStart(sel, total, size) {
5747
6109
  if (total <= size) return 0;
@@ -5792,7 +6154,7 @@ function tailLines(text, max) {
5792
6154
  const lines = text.split("\n");
5793
6155
  return lines.length <= max ? text : lines.slice(-max).join("\n");
5794
6156
  }
5795
- var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, EXPAND_MAX_LINES, CONTEXT_NOTICE_AT;
6157
+ var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, 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, ASK_HINT, PLAN_HINT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
5796
6158
  var init_app = __esm({
5797
6159
  "src/tui/app.tsx"() {
5798
6160
  "use strict";
@@ -5816,6 +6178,7 @@ var init_app = __esm({
5816
6178
  init_session();
5817
6179
  init_counter();
5818
6180
  init_provider();
6181
+ init_network();
5819
6182
  init_render();
5820
6183
  init_commands();
5821
6184
  init_editor();
@@ -5835,8 +6198,28 @@ var init_app = __esm({
5835
6198
  MAX_INPUT_ROWS = 10;
5836
6199
  MAX_PICKER_ROWS = 12;
5837
6200
  MAX_QUEUE_ROWS = 3;
6201
+ QUEUE_MARKER = "\u23F3";
6202
+ PICKER_CURRENT = " \u2190 current";
6203
+ MIN_PICKER_LABEL_COLS = 8;
6204
+ PICKER_CHROME = 8;
6205
+ PICKER_CHROME_COMPACT = 5;
6206
+ ASK_CHROME = 4;
6207
+ PLAN_CHROME = 6;
6208
+ ASK_HINT = [
6209
+ "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc stops the turn",
6210
+ "y once \xB7 a always \xB7 n no \xB7 esc stops",
6211
+ "y/a/n \xB7 esc"
6212
+ ];
6213
+ PLAN_HINT = [
6214
+ "[y/\u23CE] yes, ask before edits \xB7 [a] yes, auto-accept edits \xB7 [n] keep planning \xB7 esc stops the turn",
6215
+ "y ask first \xB7 a auto-accept \xB7 n keep planning \xB7 esc stops",
6216
+ "y/a/n \xB7 esc"
6217
+ ];
5838
6218
  EXPAND_MAX_LINES = 400;
6219
+ FALLBACK_ROWS = 24;
6220
+ FALLBACK_COLS = 80;
5839
6221
  CONTEXT_NOTICE_AT = 0.6;
6222
+ KEEP_ALWAYS = Number.POSITIVE_INFINITY;
5840
6223
  }
5841
6224
  });
5842
6225
 
@@ -5878,6 +6261,7 @@ init_session();
5878
6261
  init_kimi();
5879
6262
  init_banner();
5880
6263
  init_browser();
6264
+ init_color();
5881
6265
 
5882
6266
  // src/title.ts
5883
6267
  var APP_TITLE = "Clixad";
@@ -5907,14 +6291,7 @@ function clearTerminalTitle(out = titleStream()) {
5907
6291
 
5908
6292
  // src/main.ts
5909
6293
  init_version();
5910
- var c = {
5911
- cyan: (s) => `\x1B[36m${s}\x1B[0m`,
5912
- green: (s) => `\x1B[32m${s}\x1B[0m`,
5913
- yellow: (s) => `\x1B[33m${s}\x1B[0m`,
5914
- red: (s) => `\x1B[31m${s}\x1B[0m`,
5915
- dim: (s) => `\x1B[2m${s}\x1B[0m`,
5916
- bold: (s) => `\x1B[1m${s}\x1B[0m`
5917
- };
6294
+ var c = palette({ isTTY: process.stdout.isTTY, env: process.env });
5918
6295
  async function main() {
5919
6296
  setTerminalTitle();
5920
6297
  process.on("exit", () => clearTerminalTitle());
@@ -5982,10 +6359,10 @@ async function login(client, config, args) {
5982
6359
  console.log(`
5983
6360
  Open ${c.cyan(start.verification_uri)} and enter code: ${c.bold(start.user_code)}
5984
6361
  `);
5985
- process.stdout.write(c.dim(" waiting for GitHub authorization\u2026 (Ctrl+C to cancel)"));
6362
+ if (c.enabled) process.stdout.write(c.dim(" waiting for GitHub authorization\u2026 (Ctrl+C to cancel)"));
5986
6363
  },
5987
6364
  tick() {
5988
- process.stdout.write(c.dim("."));
6365
+ if (c.enabled) process.stdout.write(c.dim("."));
5989
6366
  }
5990
6367
  });
5991
6368
  switch (outcome.status) {
@@ -6110,11 +6487,11 @@ async function earn(client, config) {
6110
6487
  console.log(
6111
6488
  c.dim(" Credits are granted on completion. Surveys screen people out part way through \u2014\n") + c.dim(" that pays nothing and is the normal case, so just start another one.")
6112
6489
  );
6113
- process.stdout.write(c.dim(" waiting for an offer to clear"));
6490
+ if (c.enabled) process.stdout.write(c.dim(" waiting for an offer to clear"));
6114
6491
  const deadline = Date.now() + 5 * 60 * 1e3;
6115
6492
  while (Date.now() < deadline) {
6116
6493
  await sleep4(3e3);
6117
- process.stdout.write(c.dim("."));
6494
+ if (c.enabled) process.stdout.write(c.dim("."));
6118
6495
  const balance = (await safeWallet(client))?.balance ?? before;
6119
6496
  if (balance > before) {
6120
6497
  console.log(
@@ -6154,11 +6531,11 @@ async function buyCmd(client, config, pack) {
6154
6531
  Stripe Checkout: ${c.bold(checkout.pack)} \u2014 ${checkout.credits.toLocaleString("en-US")} credits for ${c.bold("$" + checkout.price_usd.toFixed(2))}`);
6155
6532
  console.log(c.dim(` ${checkout.url}`));
6156
6533
  openBrowser(checkout.url);
6157
- process.stdout.write(c.dim(" waiting for payment to clear"));
6534
+ if (c.enabled) process.stdout.write(c.dim(" waiting for payment to clear"));
6158
6535
  const deadline = Date.now() + 5 * 60 * 1e3;
6159
6536
  while (Date.now() < deadline) {
6160
6537
  await sleep4(3e3);
6161
- process.stdout.write(c.dim("."));
6538
+ if (c.enabled) process.stdout.write(c.dim("."));
6162
6539
  const balance = (await safeWallet(client))?.balance ?? before;
6163
6540
  if (balance > before) {
6164
6541
  console.log(c.green(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.11",
3
+ "version": "0.0.1-beta.13",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -39,7 +39,11 @@
39
39
  "ink-testing-library": "^4.0.0"
40
40
  },
41
41
  "dependencies": {
42
+ "@adtention/sdk": "^0.4.0",
42
43
  "ink": "^7.1.1",
43
- "react": "^19.2.8"
44
+ "react": "^19.2.8",
45
+ "slice-ansi": "^9.0.0",
46
+ "string-width": "^8.2.0",
47
+ "wrap-ansi": "^10.0.0"
44
48
  }
45
49
  }