jaitus 0.3.22 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaitus",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
4
4
  "description": "Jaitus — a coding agent in your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "node": ">=20"
17
17
  },
18
18
  "scripts": {
19
- "test": "node tests/source-sanity.js && node tests/react-build.js && node tests/stalls.js && node tests/clipboard-write.js && node tests/rewind-images.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js && node tests/mouse-e2e.js && node tests/environment.js && node tests/update.js && node tests/service-notices.js && node tests/continuation.js && node tests/steps.js && node tests/files.js && node tests/processes.js && node tests/security.js && node tests/context-budget.js && node tests/context-trim.js && node tests/compaction.js && node tests/copy-clean.js && node tests/parallel-tools.js && node tests/plan-percent.js && node tests/orb.js && node tests/project-identity.js",
19
+ "test": "node tests/source-sanity.js && node tests/react-build.js && node tests/stalls.js && node tests/clipboard-write.js && node tests/rewind-images.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js && node tests/mouse-e2e.js && node tests/environment.js && node tests/update.js && node tests/service-notices.js && node tests/continuation.js && node tests/steps.js && node tests/files.js && node tests/processes.js && node tests/security.js && node tests/context-budget.js && node tests/context-trim.js && node tests/compaction.js && node tests/copy-clean.js && node tests/parallel-tools.js && node tests/plan-percent.js && node tests/orb.js && node tests/update-required.js && node tests/select-scroll.js && node tests/project-identity.js",
20
20
  "preview": "node scripts/tui-preview.js",
21
21
  "prepublishOnly": "npm test"
22
22
  },
package/src/api.js CHANGED
@@ -94,7 +94,7 @@ export function createClient() {
94
94
 
95
95
  // x-jaitus-features: what this build understands, so the gateway can use newer error
96
96
  // codes (a used-up usage window) without an older CLI reading them as "went wrong".
97
- const base = { 'Content-Type': 'application/json', 'user-agent': `jaitus-cli/${version()}`, 'x-jaitus-features': 'usage-window plan-expired' };
97
+ const base = { 'Content-Type': 'application/json', 'user-agent': `jaitus-cli/${version()}`, 'x-jaitus-features': 'usage-window plan-expired update-required' };
98
98
  // x-jaitus-device: this computer's hashed identifiers (machine.js), which the free
99
99
  // tier is limited by. Built once per process.
100
100
  const headers = (token) => ({ ...base, 'x-jaitus-device': machineHeader(), authorization: `Bearer ${token}` });
package/src/errors.js CHANGED
@@ -16,6 +16,8 @@ const CATALOG = {
16
16
  free_machine_limit: { title: 'No free prompts on this account', hint: 'Subscribe at jaitus.com/billing to keep using Jaitus on this account.' },
17
17
  free_needs_check: { title: 'Unlock your free prompts', hint: 'A one-time ₹1 UPI check at jaitus.com/billing (refunded). Or subscribe.' },
18
18
  free_needs_cli: { title: 'Update Jaitus for free prompts', hint: 'Run npm i -g jaitus, then try again.' },
19
+ // This version is below what Jaitus now requires; the message names both versions.
20
+ update_required: { title: 'Update Jaitus to continue', hint: 'Run npm i -g jaitus@latest, then start jaitus again.' },
19
21
  free_request_too_large: { title: 'Too large for the free plan', hint: 'Run /compact or start a new session, or subscribe at jaitus.com/billing.' },
20
22
  plan_limit_reached: { title: 'Monthly usage used up', hint: 'To keep going now, switch plans at jaitus.com/billing — what is left of this plan carries over. Buying another 30 days of the same plan adds days but does not refill this month. /usage shows where you are.' },
21
23
  rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
@@ -75,6 +75,15 @@ export function lineFor(item, where = statusAddress()) {
75
75
  if (until) return `Jaitus is down for maintenance until ${until}. Updates: ${where}`;
76
76
  return `Jaitus is down for maintenance. Updates: ${where}`;
77
77
  }
78
+ if (item.kind === 'offer') {
79
+ // An offer from Admin → Offers: its headline, the short line under it, the code if it
80
+ // has one, and where to take it.
81
+ if (!title) return null;
82
+ const details = dropStop(clean(item.details, 90));
83
+ const code = clean(item.code, 30);
84
+ const billing = where.replace(/\/status$/, '/billing');
85
+ return `${title}${details ? ` — ${details}` : ''}.${code ? ` Code ${code}.` : ''} ${billing}`;
86
+ }
78
87
  if (item.kind === 'maintenance' && item.phase === 'upcoming') {
79
88
  const when = clean(item.window, 60);
80
89
  if (!when) return null;
package/src/tui-ink.js CHANGED
@@ -39,6 +39,7 @@ import { badgeText, badgeTop, turnShareText } from './planView.js';
39
39
 
40
40
  const h = React.createElement;
41
41
  const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
42
+
42
43
  const ascii = () => process.env.JAITUS_ASCII === '1';
43
44
  const g = (uni, plainText) => (ascii() ? plainText : uni);
44
45
 
@@ -419,8 +420,9 @@ function layoutTranscript(state, width, height, top) {
419
420
 
420
421
  // The drag handler needs the text under the pointer, and it must be the text actually
421
422
  // on screen rather than the transcript's idea of it.
422
- state.screen = { top: screenTop, left: 2, rows: visible };
423
+ state.screen = { top: screenTop, left: 2, rows: visible, first, all: rows, height };
423
424
  state.totalRows = rows.length;
425
+ anchorSelection(state);
424
426
 
425
427
  // The sticky header names the prompt whose answer you are looking at — the last one
426
428
  // that has scrolled off the top. Scroll far enough that the prompt itself is back on
@@ -554,6 +556,41 @@ const URL_IN_TEXT = /https?:\/\/[^\s<>"'`]+/g;
554
556
  // background colour, then CSI ? … c device attributes — alone or run together.
555
557
  export const TERMINAL_REPLY = /^(?=[\x1b\][])(?:\x1b?\]11;rgb:[0-9a-f]{1,4}\/[0-9a-f]{1,4}\/[0-9a-f]{1,4}(?:\x07|\x1b\\)?)?(?:\x1b?\[\?[0-9;]*c)?$/i;
556
558
 
559
+ /**
560
+ * Keep a drag selection on the TEXT while the view scrolls.
561
+ *
562
+ * A selection that starts in the transcript records the row of the whole transcript it
563
+ * started on, so scrolling (the wheel, or holding the pointer past an edge) moves the
564
+ * highlight with the text instead of leaving it on the same screen cells over different
565
+ * text. While the button is held the other end follows the pointer: whichever row is under
566
+ * it now, clamped to what is on screen — so with the pointer above the conversation it is
567
+ * the top row, and the selection grows as more scrolls in. Screen positions are derived
568
+ * from rows here, every frame, so painting and copying read the same thing.
569
+ */
570
+ function selectionFirstRow(state) {
571
+ const s = state.screen;
572
+ if (!s?.all) return 0;
573
+ const total = s.all.length;
574
+ const offset = state.viewingTranscript ? clamp(state.transcriptOffset || 0, 0, Math.max(0, total - s.height)) : 0;
575
+ return Math.max(0, total - offset - s.height);
576
+ }
577
+
578
+ function anchorSelection(state) {
579
+ const sel = state.selection;
580
+ const s = state.screen;
581
+ if (!sel?.anchor || !sel.focus || sel.anchor.row == null || !s?.all) return;
582
+ const first = selectionFirstRow(state);
583
+ const shown = Math.min(s.height, s.all.length - first);
584
+ if (sel.dragging && sel.pointer) {
585
+ const lastRow = first + Math.max(0, shown - 1);
586
+ const row = clamp(first + (sel.pointer.y - s.top), first, lastRow);
587
+ const x = sel.pointer.y < s.top ? s.left : sel.pointer.y > s.top + shown - 1 ? 1e4 : sel.pointer.x;
588
+ sel.focus = { x, row };
589
+ }
590
+ sel.anchor.y = s.top + sel.anchor.row - first;
591
+ if (sel.focus.row != null) sel.focus.y = s.top + sel.focus.row - first;
592
+ }
593
+
557
594
  /**
558
595
  * Draw the drag selection.
559
596
  *
@@ -1772,6 +1809,43 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
1772
1809
  notifyNow();
1773
1810
  }
1774
1811
 
1812
+ /** The row of the whole transcript at screen line `y`, or null off the transcript. */
1813
+ function transcriptRowAt(y) {
1814
+ const s = state.screen;
1815
+ if (!s?.all || state.dialog || state.panel) return null;
1816
+ if (y < s.top || y > s.top + s.rows.length - 1) return null;
1817
+ return selectionFirstRow(state) + (y - s.top);
1818
+ }
1819
+
1820
+ // Holding a drag past the top or bottom of the conversation scrolls it, faster the
1821
+ // further out the pointer is, until the button is released or the pointer comes back —
1822
+ // what every terminal does. Terminals only report the mouse when it moves, so a pointer
1823
+ // held still past the edge needs this timer to keep going.
1824
+ let edgeTimer = null;
1825
+ function stopEdgeScroll() { if (edgeTimer) { clearInterval(edgeTimer); edgeTimer = null; } }
1826
+ function edgeOut() {
1827
+ const sel = state.selection;
1828
+ const s = state.screen;
1829
+ if (!sel?.dragging || !sel.pointer || !s?.rows?.length) return 0;
1830
+ const bottom = s.top + s.rows.length - 1;
1831
+ if (sel.pointer.y < s.top) return s.top - sel.pointer.y; // above: positive = back into history
1832
+ if (sel.pointer.y > bottom) return -(sel.pointer.y - bottom); // below: towards the newest
1833
+ return 0;
1834
+ }
1835
+ function edgeScroll() {
1836
+ if (!edgeOut()) return stopEdgeScroll();
1837
+ if (edgeTimer) return;
1838
+ edgeTimer = setInterval(() => {
1839
+ const out = edgeOut();
1840
+ if (!out) return stopEdgeScroll();
1841
+ const step = Math.min(4, Math.abs(out));
1842
+ scroll(out > 0 ? step : -step);
1843
+ anchorSelection(state);
1844
+ notifyNow();
1845
+ }, 60);
1846
+ edgeTimer.unref?.();
1847
+ }
1848
+
1775
1849
  // ── selecting text in the composer ──
1776
1850
  //
1777
1851
  // The composer is a text field, so it selects like one: drag with the mouse (copied on
@@ -1857,15 +1931,28 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
1857
1931
  // out or replaced by what it stands for, and a wrapped line rejoins the line it was
1858
1932
  // wrapped from (tui/ansi.js copyText / softJoin).
1859
1933
  const pieces = [];
1860
- screen.rows.forEach((row, index) => {
1861
- const y = screen.top + index;
1862
- if (y < from.y || y > to.y) return;
1934
+ // `startX` / `endX` are screen columns of the first and last selected row (or null for
1935
+ // "the whole row").
1936
+ const take = (row, startX, endX) => {
1863
1937
  if (isDecorationRow(row)) return;
1864
1938
  const len = rowText(row).length;
1865
- const start = y === from.y ? Math.max(0, from.x - screen.left) : 0;
1866
- const end = y === to.y ? Math.max(0, to.x - screen.left + 1) : len;
1939
+ const start = startX == null ? 0 : Math.max(0, startX - screen.left);
1940
+ const end = endX == null ? len : Math.min(len, Math.max(0, endX - screen.left + 1));
1867
1941
  pieces.push({ text: copyText(row, start, end), join: pieces.length ? softJoin(row) : null });
1868
- });
1942
+ };
1943
+ if (a.row != null && b.row != null && screen.all) {
1944
+ // Anchored to the text: everything selected, including what has scrolled off screen.
1945
+ const [f, t] = (a.row < b.row || (a.row === b.row && a.x <= b.x)) ? [a, b] : [b, a];
1946
+ for (let r = f.row; r <= t.row; r++) {
1947
+ if (screen.all[r]) take(screen.all[r], r === f.row ? f.x : null, r === t.row ? t.x : null);
1948
+ }
1949
+ } else {
1950
+ screen.rows.forEach((row, index) => {
1951
+ const y = screen.top + index;
1952
+ if (y < from.y || y > to.y) return;
1953
+ take(row, y === from.y ? from.x : null, y === to.y ? to.x : null);
1954
+ });
1955
+ }
1869
1956
  let out = '';
1870
1957
  pieces.forEach((piece, i) => {
1871
1958
  // A word cut mid-way (join '') keeps every character on both sides; anywhere else
@@ -1912,6 +1999,7 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
1912
1999
  }
1913
2000
 
1914
2001
  function clearSelection() {
2002
+ stopEdgeScroll();
1915
2003
  if (!state.selection) return;
1916
2004
  state.selection = null;
1917
2005
  notifyNow();
@@ -2141,20 +2229,33 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
2141
2229
  // A press starts a possible drag AND a possible click. Which it was is only
2142
2230
  // known on release, so nothing is dispatched yet — dispatching here made every
2143
2231
  // attempt to select text also press whatever was under the cursor.
2144
- pressedAt = { x: event.x, y: event.y };
2232
+ pressedAt = { x: event.x, y: event.y, row: transcriptRowAt(event.y) };
2145
2233
  dragged = false;
2146
2234
  clearSelection();
2147
2235
  return;
2148
2236
  }
2149
2237
  if (event.kind === 'drag' && pressedAt) {
2150
2238
  if (event.x !== pressedAt.x || event.y !== pressedAt.y) dragged = true;
2151
- state.selection = { anchor: pressedAt, focus: { x: event.x, y: event.y } };
2239
+ if (pressedAt.row == null) {
2240
+ state.selection = { anchor: { x: pressedAt.x, y: pressedAt.y }, focus: { x: event.x, y: event.y } };
2241
+ return notifyNow();
2242
+ }
2243
+ // Anchored to the text, the other end on the pointer (anchorSelection), and the
2244
+ // view scrolls on its own while the pointer is held past either edge.
2245
+ const sel = state.selection?.dragging ? state.selection
2246
+ : { anchor: { x: pressedAt.x, y: pressedAt.y, row: pressedAt.row }, focus: { x: event.x, y: event.y }, dragging: true };
2247
+ sel.pointer = { x: event.x, y: event.y };
2248
+ state.selection = sel;
2249
+ anchorSelection(state);
2250
+ edgeScroll();
2152
2251
  return notifyNow();
2153
2252
  }
2154
2253
  if (event.kind === 'up') {
2155
2254
  const start = pressedAt;
2156
2255
  pressedAt = null;
2256
+ stopEdgeScroll();
2157
2257
  if (!start) return;
2258
+ if (state.selection?.dragging) { anchorSelection(state); state.selection.dragging = false; }
2158
2259
  if (dragged) { dragged = false; void finishSelection(); return; }
2159
2260
  const now = Date.now();
2160
2261
  const again = lastClick && now - lastClick.at < 450 && lastClick.y === start.y && Math.abs(lastClick.x - start.x) <= 1;