clixad 0.0.1-beta.12 → 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 +503 -188
  2. package/package.json +6 -2
package/dist/clixad.mjs CHANGED
@@ -385,25 +385,77 @@ var init_client = __esm({
385
385
  * credits, and this program is installed by strangers from npm. All the CLI
386
386
  * gets to decide is when to ask.
387
387
  */
388
- async sponsorLine(signal) {
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) {
389
403
  try {
390
- const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/line`, {
404
+ const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
391
405
  headers: this.headers(),
392
- signal
406
+ ...signal ? { signal } : {}
393
407
  });
394
408
  if (!res.ok) return null;
395
409
  const body = await res.json();
396
- if (typeof body.text !== "string" || !body.text.trim()) return null;
410
+ const publisherId = typeof body.publisher_id === "string" ? body.publisher_id.trim() : "";
411
+ if (!publisherId) return null;
397
412
  return {
398
- provider: "adtention",
399
- id: typeof body.id === "string" && body.id ? body.id : "adtention",
400
- text: body.text.trim(),
401
- credits: typeof body.credits === "number" ? body.credits : 0
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 } : {}
402
417
  };
403
418
  } catch {
404
419
  return null;
405
420
  }
406
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
+ }
407
459
  /**
408
460
  * Search the web, through the gateway.
409
461
  *
@@ -3661,6 +3713,8 @@ If it's installed under a different name/path, set CLIXAD_KIMI_BIN.`;
3661
3713
 
3662
3714
  // src/banner.ts
3663
3715
  import { homedir as homedir6 } from "node:os";
3716
+ import sliceAnsi from "slice-ansi";
3717
+ import stringWidth from "string-width";
3664
3718
  function duckLines() {
3665
3719
  const lines = [];
3666
3720
  for (let row = 0; row < DUCK.length; row += 2) {
@@ -3744,22 +3798,29 @@ function welcomeBanner(opts) {
3744
3798
  return out.join("\n");
3745
3799
  }
3746
3800
  function compactBanner(opts) {
3747
- 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));
3748
3803
  const hint = bannerTip(opts.tipIndex);
3749
- return duckLines().map((d) => " " + d).join("\n") + `
3750
-
3751
- ${BOLD}${TEXT}${welcome}${R2}
3752
- ${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}
3753
3811
  `;
3754
3812
  }
3755
- function clearScreen() {
3756
- const out = process.stdout;
3757
- if (!out.isTTY && !process.stdin.isTTY) return;
3813
+ function clearScreen(out = process.stdout) {
3814
+ if (!out.isTTY) return;
3758
3815
  out.write("\n".repeat(out.rows ?? 40));
3759
3816
  out.write("\x1B[2J\x1B[3J\x1B[H\x1B[0f");
3760
3817
  }
3761
3818
  function visibleLength(s) {
3762
- 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);
3763
3824
  }
3764
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;
3765
3826
  var init_banner = __esm({
@@ -3802,7 +3863,7 @@ var init_banner = __esm({
3802
3863
  DUCK_W = 14;
3803
3864
  cell = (text, w) => ({ text, w });
3804
3865
  padTo = (c2, width) => (c2?.text ?? "") + " ".repeat(Math.max(0, width - (c2?.w ?? 0)));
3805
- 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;
3806
3867
  BANNER_TIPS = [
3807
3868
  { key: "/init", desc: "make a CLIXAD.md" },
3808
3869
  { key: "shift+tab", desc: "switch mode" },
@@ -3824,6 +3885,45 @@ var init_banner = __esm({
3824
3885
  }
3825
3886
  });
3826
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
+
3827
3927
  // src/version.ts
3828
3928
  import { readFileSync as readFileSync6 } from "node:fs";
3829
3929
  function readVersion() {
@@ -4050,7 +4150,7 @@ function adtentionSource(fetchLine) {
4050
4150
  if (!line2) return null;
4051
4151
  const text = line2.text.trim();
4052
4152
  if (!text) return null;
4053
- return { id: line2.id, text };
4153
+ return { ...line2, text };
4054
4154
  }
4055
4155
  };
4056
4156
  }
@@ -4074,19 +4174,98 @@ function sponsorEnabled({ isTTY, env, configEnabled }) {
4074
4174
  if (env.CI !== void 0 && env.CI !== "") return false;
4075
4175
  return true;
4076
4176
  }
4077
- function sponsorText(line2, cols) {
4078
- const indent = " ";
4079
- const prefix = `${indent}${SPONSOR_MARKER} `;
4080
- const room = cols - prefix.length;
4081
- if (room < 12) return null;
4082
- const body = line2.length <= room ? line2 : `${line2.slice(0, Math.max(0, room - 1)).trimEnd()}\u2026`;
4083
- 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;
4084
4193
  }
4085
- 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;
4086
4195
  var init_render = __esm({
4087
4196
  "src/sponsor/render.ts"() {
4088
4197
  "use strict";
4089
- 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;
4090
4269
  }
4091
4270
  });
4092
4271
 
@@ -4450,48 +4629,42 @@ var init_markdown = __esm({
4450
4629
  // src/tui/views.tsx
4451
4630
  import "react";
4452
4631
  import { Box, Text } from "ink";
4632
+ import wrapAnsi from "wrap-ansi";
4453
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
+ }
4454
4637
  function lineCount(text, cols) {
4455
- 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;
4456
4639
  }
4457
4640
  function tailRows(text, cols, max) {
4458
4641
  if (max <= 0) return "";
4459
- const width = Math.max(1, cols);
4460
- const lines = text.split("\n");
4461
- const kept = [];
4462
- let used = 0;
4463
- for (let i = lines.length - 1; i >= 0; i--) {
4464
- const line2 = lines[i];
4465
- const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
4466
- if (used + h <= max) {
4467
- kept.unshift(line2);
4468
- used += h;
4469
- continue;
4470
- }
4471
- const room = max - used;
4472
- if (room > 0) kept.unshift(line2.slice(-(room * width)));
4473
- break;
4474
- }
4475
- 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);
4476
4662
  }
4477
4663
  function headRows(text, cols, max) {
4478
4664
  if (max <= 0) return "";
4479
- const width = Math.max(1, cols);
4480
- const lines = text.split("\n");
4481
- const kept = [];
4482
- let used = 0;
4483
- for (const line2 of lines) {
4484
- const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
4485
- if (used + h <= max) {
4486
- kept.push(line2);
4487
- used += h;
4488
- continue;
4489
- }
4490
- const room = max - used;
4491
- if (room > 0) kept.push(line2.slice(0, room * width));
4492
- break;
4493
- }
4494
- 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");
4495
4668
  }
4496
4669
  function errorNotice(message, rows = ERROR_NOTICE_ROWS) {
4497
4670
  const clean = message.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)?/g, " ").replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g, " ");
@@ -4546,7 +4719,7 @@ function EntryView({ entry }) {
4546
4719
  ) });
4547
4720
  }
4548
4721
  }
4549
- 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;
4550
4723
  var init_views = __esm({
4551
4724
  "src/tui/views.tsx"() {
4552
4725
  "use strict";
@@ -4555,37 +4728,52 @@ var init_views = __esm({
4555
4728
  MANGO3 = "#f5b841";
4556
4729
  MANGO_BRIGHT = "#ffcf6b";
4557
4730
  SLATE = "#8b98ae";
4731
+ AD_TEXT = "#8a80b4";
4732
+ AD_MARK = "#6b6390";
4558
4733
  MODE_STYLE = {
4559
4734
  normal: { color: SLATE, glyph: "\u23F5" },
4560
4735
  acceptEdits: { color: "#7ee787", glyph: "\u23F5\u23F5" },
4561
4736
  plan: { color: "#79c0ff", glyph: "\u23F8" }
4562
4737
  };
4738
+ WRAP_LIKE_INK = { trim: false, hard: true };
4739
+ DIALOG_CHROME_COLS = 4;
4563
4740
  ERROR_NOTICE_ROWS = 12;
4564
4741
  ERROR_NOTICE_COLS = 200;
4565
4742
  }
4566
4743
  });
4567
4744
 
4568
4745
  // src/tui/app.tsx
4569
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4746
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4570
4747
  import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
4571
4748
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4572
4749
  function useTerminalSize() {
4573
- const [size, setSize] = useState({
4574
- rows: process.stdout.rows ?? 24,
4575
- 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
4576
4754
  });
4577
- useEffect(() => {
4578
- const onResize = () => setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });
4579
- process.stdout.on("resize", onResize);
4580
- return () => {
4581
- process.stdout.off("resize", onResize);
4582
- };
4583
- }, []);
4584
- 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);
4585
4773
  }
4586
- function App({ client, config, wallet, session, initialTask }) {
4774
+ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
4587
4775
  const { exit } = useApp();
4588
- const { write: writeToStdout } = useStdout();
4776
+ const { stdout, write: writeToStdout } = useStdout();
4589
4777
  const idRef = useRef(1);
4590
4778
  const { rows, cols } = useTerminalSize();
4591
4779
  const root = process.cwd();
@@ -4593,8 +4781,12 @@ function App({ client, config, wallet, session, initialTask }) {
4593
4781
  {
4594
4782
  id: 0,
4595
4783
  kind: "banner",
4596
- text: ((process.stdout.columns ?? 80) >= MIN_BANNER_WIDTH ? welcomeBanner : compactBanner)({
4597
- 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,
4598
4790
  name: config.login ?? config.email?.split("@")[0],
4599
4791
  model: config.model,
4600
4792
  balance: wallet?.balance ?? 0,
@@ -4658,7 +4850,37 @@ function App({ client, config, wallet, session, initialTask }) {
4658
4850
  const sponsorIdxRef = useRef(0);
4659
4851
  const sponsorPrevRef = useRef(void 0);
4660
4852
  const sponsorCallRef = useRef(0);
4661
- const remoteSponsorRef = useRef(adtentionSource((signal) => client.sponsorLine(signal)));
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
+ );
4662
4884
  const tallyRef = useRef(createTally());
4663
4885
  const heightCacheRef = useRef(/* @__PURE__ */ new Map());
4664
4886
  const push = useCallback((e) => {
@@ -4748,7 +4970,7 @@ function App({ client, config, wallet, session, initialTask }) {
4748
4970
  (modelId) => {
4749
4971
  const call = ++sponsorCallRef.current;
4750
4972
  if (!sponsorEnabled({
4751
- isTTY: process.stdout.isTTY,
4973
+ isTTY: stdout.isTTY,
4752
4974
  env: process.env,
4753
4975
  configEnabled: config.sponsor
4754
4976
  })) {
@@ -4764,19 +4986,28 @@ function App({ client, config, wallet, session, initialTask }) {
4764
4986
  if (picked) {
4765
4987
  sponsorPrevRef.current = picked.id;
4766
4988
  recordImpression(tallyRef.current, picked.id);
4767
- setSponsor(picked.text);
4989
+ setSponsor({ text: picked.text, origin: "house" });
4768
4990
  } else {
4769
4991
  setSponsor(null);
4770
4992
  }
4993
+ if (!adFitsWidth(colsRef.current)) return;
4771
4994
  void remoteSponsorRef.current.fetch().then((line2) => {
4772
4995
  if (!line2 || call !== sponsorCallRef.current) return;
4996
+ if (!sponsorRow(line2.text, colsRef.current, "network")) return;
4773
4997
  sponsorPrevRef.current = line2.id;
4774
4998
  recordImpression(tallyRef.current, line2.id);
4775
- setSponsor(line2.text);
4999
+ setSponsor({ text: line2.text, origin: "network" });
5000
+ pendingImpressionRef.current = { call, impression: line2.impression };
4776
5001
  });
4777
5002
  },
4778
- [config.sponsor, wallet]
5003
+ [config.sponsor, stdout, wallet]
4779
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]);
4780
5011
  const askUser = useCallback(
4781
5012
  (req) => new Promise((resolve2) => setAsk({ req, resolve: resolve2 })),
4782
5013
  []
@@ -4861,7 +5092,7 @@ function App({ client, config, wallet, session, initialTask }) {
4861
5092
  return;
4862
5093
  }
4863
5094
  const lines = event.result.split("\n").filter((l) => l.trim() !== "");
4864
- 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)));
4865
5096
  const more = Math.max(0, lines.length - shown.length);
4866
5097
  const id = push({
4867
5098
  kind: "tool",
@@ -5000,6 +5231,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5000
5231
  dropDelta();
5001
5232
  setLive(null);
5002
5233
  setBusy(false);
5234
+ reportSponsor();
5003
5235
  sponsorCallRef.current++;
5004
5236
  setSponsor(null);
5005
5237
  }
@@ -5012,6 +5244,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5012
5244
  dropDelta,
5013
5245
  handleEvent,
5014
5246
  model,
5247
+ reportSponsor,
5015
5248
  nextSponsor,
5016
5249
  permit,
5017
5250
  push,
@@ -5330,7 +5563,14 @@ ${NO_SEARCH_INSTRUCTION}`),
5330
5563
  push({
5331
5564
  kind: "notice",
5332
5565
  text: list.map(
5333
- (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
+ )
5334
5574
  ).join("\n").concat("\n /bg kill <id> \xB7 /bg kill all")
5335
5575
  });
5336
5576
  return;
@@ -5384,7 +5624,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5384
5624
  { command }
5385
5625
  );
5386
5626
  const lines = output.split("\n").filter((l) => l.trim() !== "");
5387
- 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)));
5388
5628
  const more = Math.max(0, lines.length - shown.length);
5389
5629
  const id = push({
5390
5630
  kind: "tool",
@@ -5624,36 +5864,82 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
5624
5864
  });
5625
5865
  const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
5626
5866
  const spinner = spinnerRef.current[Math.floor(tick / SPINNER_TICKS_PER_FRAME) % spinnerRef.current.length];
5627
- const sponsorLine = busy && sponsor ? sponsorText(sponsor, cols) : null;
5628
- 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);
5629
5870
  const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
5630
5871
  const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
5631
- const inputBoxHeight = 2 + inputRows;
5632
- const queueRows = [
5633
- ...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, " ")),
5634
5874
  ...queueView.length > MAX_QUEUE_ROWS ? [`+${queueView.length - MAX_QUEUE_ROWS} more queued`] : []
5635
- ];
5636
- const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0) + queueRows.length;
5637
- let budget = Math.max(0, viewport - chromeHeight - menu.length);
5638
- const PICKER_CHROME = 8;
5639
- 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;
5640
5895
  const pickerFrom = picker ? windowStart(pickerSel, picker.items.length, pickerRows) : 0;
5641
5896
  const pickerItems = picker ? picker.items.slice(pickerFrom, pickerFrom + pickerRows) : [];
5642
- const pickerHeight = picker ? pickerItems.length + PICKER_CHROME : 0;
5897
+ const pickerHeight = picker ? pickerItems.length + pickerChrome : 0;
5643
5898
  pickerWindowRef.current = { from: pickerFrom, count: pickerItems.length };
5644
- 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
+ ) : "";
5645
5934
  budget -= pickerHeight;
5646
- const ASK_CHROME = 4;
5647
- const askSummaryRows = ask2 ? lineCount(ask2.req.summary, cols) : 0;
5648
- const askPreview = ask2?.req.preview ? headRows(ask2.req.preview, cols, Math.max(1, budget - ASK_CHROME - askSummaryRows)) : "";
5649
- 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;
5650
5939
  budget -= askHeight;
5651
- const PLAN_CHROME = 6;
5652
- const planBlock = plan ? headRows(plan.plan, cols, Math.max(1, budget - PLAN_CHROME)) : "";
5653
- 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;
5654
5942
  budget -= planHeight;
5655
- const busyHeight = busy ? 2 : 0;
5656
- budget -= busyHeight;
5657
5943
  const liveRaw = [
5658
5944
  // Trailing blank lines are rows the clamp would spend on nothing, and a
5659
5945
  // streamed answer ends on one more often than not.
@@ -5677,7 +5963,10 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
5677
5963
  }, [entries, cols]);
5678
5964
  const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + busyHeight + liveHeight;
5679
5965
  const spacer = Math.max(0, viewport - printed - used);
5680
- 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
+ );
5681
5970
  const contextPct = useMemo(() => {
5682
5971
  const window = contextWindow(model);
5683
5972
  if (!window) return 0;
@@ -5687,81 +5976,86 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
5687
5976
  ]);
5688
5977
  return used2 / window;
5689
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} `)));
5690
5997
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
5691
5998
  /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }, staticEpoch),
5692
5999
  spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
5693
6000
  liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
5694
- busy ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
6001
+ busyHeight > 0 ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
5695
6002
  /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
5696
6003
  spinner,
5697
6004
  " "
5698
6005
  ] }),
5699
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
5700
- busyLabel,
5701
- " ",
5702
- elapsed,
5703
- "s \xB7 esc to stop"
5704
- ] })
6006
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: busyText })
5705
6007
  ] }) : null,
5706
6008
  ask2 ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
5707
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: ask2.req.summary }),
6009
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: askSummary }),
5708
6010
  askPreview ? /* @__PURE__ */ jsx2(Text2, { children: askPreview }) : null,
5709
- /* @__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) })
5710
6012
  ] }) : null,
5711
6013
  plan ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MODE_STYLE.plan.color, paddingX: 1, children: [
5712
- /* @__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) }),
5713
6015
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
5714
- /* @__PURE__ */ jsx2(Text2, { children: renderMarkdown(planBlock) }),
5715
- /* @__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) })
5716
6018
  ] }) : null,
5717
6019
  picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
5718
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: picker.title }),
5719
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.subtitle }),
5720
- /* @__PURE__ */ jsx2(Box2, { height: 1 }),
5721
- pickerItems.map((item, i) => {
5722
- const index = pickerFrom + i;
5723
- return /* @__PURE__ */ jsxs2(Box2, { children: [
5724
- /* @__PURE__ */ jsxs2(Text2, { color: index === pickerSel ? MANGO_BRIGHT : void 0, bold: index === pickerSel, children: [
5725
- index === pickerSel ? "\u276F " : " ",
5726
- `${i + 1}. `,
5727
- item.label.padEnd(pickerLabelW + 2)
5728
- ] }),
5729
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
5730
- item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
5731
- ] }, item.value);
5732
- }),
5733
- /* @__PURE__ */ jsx2(Box2, { height: 1 }),
5734
- /* @__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 })
5735
6030
  ] }) : null,
5736
6031
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
5737
- queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u23F3 ${line2}` }, i)),
5738
- sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
5739
- /* @__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: [
5740
6046
  /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
5741
6047
  /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
5742
- ] }),
5743
- menu.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
5744
- /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? MANGO_BRIGHT : MANGO3, bold: i === sel, children: [
5745
- i === sel ? " \u276F " : " ",
5746
- item.label.padEnd(labelW + 2)
5747
- ] }),
5748
- /* @__PURE__ */ jsx2(Text2, { dimColor: i !== sel, children: item.hint })
5749
- ] }, item.value)),
5750
- quitHint || menu.length > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
5751
- " ",
5752
- quitHint ? "press ctrl+c again to quit" : "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close"
5753
6048
  ] }) : /* @__PURE__ */ jsxs2(Box2, { children: [
5754
- /* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, children: ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}` }),
5755
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
5756
- " ",
5757
- statusLine({
5758
- model,
5759
- balance,
5760
- spent,
5761
- contextPct,
5762
- minutes: (Date.now() - runStartedAtRef.current) / 6e4
5763
- })
5764
- ] })
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 })
5765
6059
  ] })
5766
6060
  ] })
5767
6061
  ] });
@@ -5793,17 +6087,23 @@ function renderInput(state, from, rows) {
5793
6087
  ] }, row);
5794
6088
  });
5795
6089
  }
5796
- function statusLine(o) {
6090
+ function statusLine(o, cols) {
5797
6091
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
5798
- const parts = [
5799
- o.model,
5800
- `${o.balance.toLocaleString("en-US")} cr`,
5801
- o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0,
5802
- burn,
5803
- o.contextPct >= CONTEXT_NOTICE_AT ? `context ${Math.round(o.contextPct * 100)}%` : void 0,
5804
- "/help"
5805
- ].filter(Boolean);
5806
- 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);
5807
6107
  }
5808
6108
  function windowStart(sel, total, size) {
5809
6109
  if (total <= size) return 0;
@@ -5854,7 +6154,7 @@ function tailLines(text, max) {
5854
6154
  const lines = text.split("\n");
5855
6155
  return lines.length <= max ? text : lines.slice(-max).join("\n");
5856
6156
  }
5857
- 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;
5858
6158
  var init_app = __esm({
5859
6159
  "src/tui/app.tsx"() {
5860
6160
  "use strict";
@@ -5878,6 +6178,7 @@ var init_app = __esm({
5878
6178
  init_session();
5879
6179
  init_counter();
5880
6180
  init_provider();
6181
+ init_network();
5881
6182
  init_render();
5882
6183
  init_commands();
5883
6184
  init_editor();
@@ -5897,8 +6198,28 @@ var init_app = __esm({
5897
6198
  MAX_INPUT_ROWS = 10;
5898
6199
  MAX_PICKER_ROWS = 12;
5899
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
+ ];
5900
6218
  EXPAND_MAX_LINES = 400;
6219
+ FALLBACK_ROWS = 24;
6220
+ FALLBACK_COLS = 80;
5901
6221
  CONTEXT_NOTICE_AT = 0.6;
6222
+ KEEP_ALWAYS = Number.POSITIVE_INFINITY;
5902
6223
  }
5903
6224
  });
5904
6225
 
@@ -5940,6 +6261,7 @@ init_session();
5940
6261
  init_kimi();
5941
6262
  init_banner();
5942
6263
  init_browser();
6264
+ init_color();
5943
6265
 
5944
6266
  // src/title.ts
5945
6267
  var APP_TITLE = "Clixad";
@@ -5969,14 +6291,7 @@ function clearTerminalTitle(out = titleStream()) {
5969
6291
 
5970
6292
  // src/main.ts
5971
6293
  init_version();
5972
- var c = {
5973
- cyan: (s) => `\x1B[36m${s}\x1B[0m`,
5974
- green: (s) => `\x1B[32m${s}\x1B[0m`,
5975
- yellow: (s) => `\x1B[33m${s}\x1B[0m`,
5976
- red: (s) => `\x1B[31m${s}\x1B[0m`,
5977
- dim: (s) => `\x1B[2m${s}\x1B[0m`,
5978
- bold: (s) => `\x1B[1m${s}\x1B[0m`
5979
- };
6294
+ var c = palette({ isTTY: process.stdout.isTTY, env: process.env });
5980
6295
  async function main() {
5981
6296
  setTerminalTitle();
5982
6297
  process.on("exit", () => clearTerminalTitle());
@@ -6044,10 +6359,10 @@ async function login(client, config, args) {
6044
6359
  console.log(`
6045
6360
  Open ${c.cyan(start.verification_uri)} and enter code: ${c.bold(start.user_code)}
6046
6361
  `);
6047
- 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)"));
6048
6363
  },
6049
6364
  tick() {
6050
- process.stdout.write(c.dim("."));
6365
+ if (c.enabled) process.stdout.write(c.dim("."));
6051
6366
  }
6052
6367
  });
6053
6368
  switch (outcome.status) {
@@ -6172,11 +6487,11 @@ async function earn(client, config) {
6172
6487
  console.log(
6173
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.")
6174
6489
  );
6175
- 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"));
6176
6491
  const deadline = Date.now() + 5 * 60 * 1e3;
6177
6492
  while (Date.now() < deadline) {
6178
6493
  await sleep4(3e3);
6179
- process.stdout.write(c.dim("."));
6494
+ if (c.enabled) process.stdout.write(c.dim("."));
6180
6495
  const balance = (await safeWallet(client))?.balance ?? before;
6181
6496
  if (balance > before) {
6182
6497
  console.log(
@@ -6216,11 +6531,11 @@ async function buyCmd(client, config, pack) {
6216
6531
  Stripe Checkout: ${c.bold(checkout.pack)} \u2014 ${checkout.credits.toLocaleString("en-US")} credits for ${c.bold("$" + checkout.price_usd.toFixed(2))}`);
6217
6532
  console.log(c.dim(` ${checkout.url}`));
6218
6533
  openBrowser(checkout.url);
6219
- process.stdout.write(c.dim(" waiting for payment to clear"));
6534
+ if (c.enabled) process.stdout.write(c.dim(" waiting for payment to clear"));
6220
6535
  const deadline = Date.now() + 5 * 60 * 1e3;
6221
6536
  while (Date.now() < deadline) {
6222
6537
  await sleep4(3e3);
6223
- process.stdout.write(c.dim("."));
6538
+ if (c.enabled) process.stdout.write(c.dim("."));
6224
6539
  const balance = (await safeWallet(client))?.balance ?? before;
6225
6540
  if (balance > before) {
6226
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.12",
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
  }